org.kontalk.ui.GroupInfoFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.ui.GroupInfoFragment.java

Source

/*
 * Kontalk Android client
 * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
    
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
    
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
    
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.kontalk.ui;

import java.text.Collator;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.akalipetis.fragment.ActionModeListFragment;
import com.akalipetis.fragment.MultiChoiceModeListener;

import org.jxmpp.util.XmppStringUtils;
import org.spongycastle.openpgp.PGPPublicKey;
import org.spongycastle.openpgp.PGPPublicKeyRing;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.view.ActionMode;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;

import org.kontalk.Kontalk;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.client.KontalkGroupManager.KontalkGroup;
import org.kontalk.crypto.PGP;
import org.kontalk.data.Contact;
import org.kontalk.data.Conversation;
import org.kontalk.provider.Keyring;
import org.kontalk.provider.MessagesProviderUtils;
import org.kontalk.provider.MyMessages;
import org.kontalk.provider.MyMessages.Groups;
import org.kontalk.provider.MyUsers;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.ui.view.ContactsListItem;
import org.kontalk.util.SystemUtils;

/**
 * Group information fragment
 * FIXME this class is too tied to the concept of "Kontalk group"
 * @author Daniele Ricci
 */
public class GroupInfoFragment extends ActionModeListFragment
        implements Contact.ContactChangeListener, MultiChoiceModeListener {

    private TextView mTitle;
    private Button mSetSubject;
    private Button mLeave;
    private Button mIgnoreAll;
    private MenuItem mAddMenu;
    private MenuItem mRemoveMenu;
    private MenuItem mChatMenu;
    private MenuItem mReaddMenu;

    GroupMembersAdapter mMembersAdapter;

    Conversation mConversation;

    private int mCheckedItemCount;

    private BroadcastReceiver mRosterReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String jid = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
            boolean isSubscribed = intent.getBooleanExtra(MessageCenterService.EXTRA_SUBSCRIBED_FROM, false)
                    && intent.getBooleanExtra(MessageCenterService.EXTRA_SUBSCRIBED_TO, false);
            mMembersAdapter.setSubscribed(jid, isSubscribed);
        }
    };
    private LocalBroadcastManager mLocalBroadcastManager;

    public static GroupInfoFragment newInstance(long threadId) {
        GroupInfoFragment f = new GroupInfoFragment();
        Bundle data = new Bundle();
        data.putLong("conversation", threadId);
        f.setArguments(data);
        return f;
    }

    private void loadConversation(long threadId) {
        mConversation = Conversation.loadFromId(getContext(), threadId);
        mMembersAdapter.setGroupJid(mConversation.getGroupJid());
        String subject = mConversation.getGroupSubject();
        mTitle.setText(TextUtils.isEmpty(subject) ? getString(R.string.group_untitled) : subject);

        String selfJid = Authenticator.getSelfJID(getContext());
        boolean isOwner = KontalkGroup.checkOwnership(mConversation.getGroupJid(), selfJid);
        boolean isMember = mConversation.getGroupMembership() == Groups.MEMBERSHIP_MEMBER;
        mSetSubject.setEnabled(isOwner && isMember);
        mLeave.setEnabled(isMember);

        // listen to roster entry status requests
        IntentFilter filter = new IntentFilter(MessageCenterService.ACTION_ROSTER_STATUS);
        mLocalBroadcastManager.registerReceiver(mRosterReceiver, filter);

        // load members
        boolean showIgnoreAll = false;
        String[] members = getGroupMembers();
        mMembersAdapter.clear();
        for (String jid : members) {
            Contact c = Contact.findByUserId(getContext(), jid);
            if (c.isKeyChanged() || c.getTrustedLevel() == MyUsers.Keys.TRUST_UNKNOWN)
                showIgnoreAll = true;
            boolean owner = KontalkGroup.checkOwnership(mConversation.getGroupJid(), jid);
            boolean isSelfJid = jid.equalsIgnoreCase(selfJid);
            mMembersAdapter.add(c, owner, isSelfJid);
            if (!isSelfJid) {
                // request roster entry status
                MessageCenterService.requestRosterEntryStatus(getContext(), jid);
            }
        }

        mIgnoreAll.setVisibility(showIgnoreAll ? View.VISIBLE : View.GONE);

        mMembersAdapter.notifyDataSetChanged();
        updateUI();
    }

    private void updateUI() {
        String selfJid = Authenticator.getSelfJID(getContext());
        boolean isOwner = KontalkGroup.checkOwnership(mConversation.getGroupJid(), selfJid);
        if (mRemoveMenu != null) {
            mRemoveMenu.setVisible(isOwner);
        }
        if (mAddMenu != null) {
            mAddMenu.setVisible(isOwner);
        }
        if (mReaddMenu != null) {
            mReaddMenu.setVisible(isOwner);
        }
    }

    private String[] getGroupMembers() {
        String[] members = mConversation.getGroupPeers();
        String[] added = MessagesProviderUtils.getGroupMembers(getContext(), mConversation.getGroupJid(),
                Groups.MEMBER_PENDING_ADDED);
        if (added.length > 0)
            members = SystemUtils.concatenate(members, added);
        // if we are in the group, add ourself to the list
        if (mConversation.getGroupMembership() == Groups.MEMBERSHIP_MEMBER) {
            String selfJid = Authenticator.getSelfJID(getContext());
            members = SystemUtils.concatenate(members, selfJid);
        }
        return members;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mMembersAdapter = new GroupMembersAdapter(getContext(), null);
        setListAdapter(mMembersAdapter);
        setMultiChoiceModeListener(this);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.group_info, container, false);

        mTitle = (TextView) view.findViewById(R.id.title);

        mSetSubject = (Button) view.findViewById(R.id.btn_change_title);
        mSetSubject.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new MaterialDialog.Builder(getContext()).title(R.string.title_group_subject)
                        .positiveText(android.R.string.ok).negativeText(android.R.string.cancel)
                        .input(null, mConversation.getGroupSubject(), true, new MaterialDialog.InputCallback() {
                            @Override
                            public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {
                                setGroupSubject(!TextUtils.isEmpty(input) ? input.toString() : null);
                            }
                        }).inputRange(0, Groups.GROUP_SUBJECT_MAX_LENGTH).show();
            }
        });
        mLeave = (Button) view.findViewById(R.id.btn_leave);
        mLeave.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                confirmLeave();
            }
        });
        mIgnoreAll = (Button) view.findViewById(R.id.btn_ignore_all);
        mIgnoreAll.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new MaterialDialog.Builder(getContext()).title(R.string.title_ignore_all_identities)
                        .content(R.string.msg_ignore_all_identities).positiveText(android.R.string.ok)
                        .positiveColorRes(R.color.button_danger)
                        .onPositive(new MaterialDialog.SingleButtonCallback() {
                            @Override
                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                                mMembersAdapter.ignoreAll();
                                reload();
                            }
                        }).negativeText(android.R.string.cancel).show();
            }
        });

        return view;
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.group_info_menu, menu);
        mAddMenu = menu.findItem(R.id.menu_invite);
    }

    void setGroupSubject(String subject) {
        mConversation.setGroupSubject(subject);
        reload();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // action mode is active - no processing
        if (isActionModeActive())
            return true;

        switch (item.getItemId()) {
        case R.id.menu_invite:
            Activity parent = getActivity();
            if (parent != null) {
                parent.setResult(GroupInfoActivity.RESULT_ADD_USERS, null);
                parent.finish();
            }
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    public boolean isActionModeActive() {
        return mCheckedItemCount > 0;
    }

    @Override
    public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
        if (checked)
            mCheckedItemCount++;
        else
            mCheckedItemCount--;
        mode.setTitle(
                getResources().getQuantityString(R.plurals.context_selected, mCheckedItemCount, mCheckedItemCount));
        mode.invalidate();
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_remove:
            // using clone because listview returns its original copy
            removeSelectedUsers(SystemUtils.cloneSparseBooleanArray(getListView().getCheckedItemPositions()));
            mode.finish();
            return true;
        case R.id.menu_add_again:
            // using clone because listview returns its original copy
            readdUser(SystemUtils.cloneSparseBooleanArray(getListView().getCheckedItemPositions()));
            return true;
        case R.id.menu_chat:
            openChat(getCheckedItem().contact.getJID());
            return true;
        }
        return false;
    }

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        MenuInflater inflater = mode.getMenuInflater();
        inflater.inflate(R.menu.group_info_ctx, menu);
        mRemoveMenu = menu.findItem(R.id.menu_remove);
        mChatMenu = menu.findItem(R.id.menu_chat);
        mReaddMenu = menu.findItem(R.id.menu_add_again);
        updateUI();
        return true;
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {
        mCheckedItemCount = 0;
        getListView().clearChoices();
        mMembersAdapter.notifyDataSetChanged();
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
        mChatMenu.setVisible(mCheckedItemCount == 1);
        return true;
    }

    private GroupMembersAdapter.GroupMember getCheckedItem() {
        if (mCheckedItemCount != 1)
            throw new IllegalStateException("checked items count must be exactly 1");

        return (GroupMembersAdapter.GroupMember) getListView().getItemAtPosition(getCheckedItemPosition());
    }

    private int getCheckedItemPosition() {
        SparseBooleanArray checked = getListView().getCheckedItemPositions();
        return checked.keyAt(checked.indexOfValue(true));
    }

    private void removeSelectedUsers(final SparseBooleanArray checked) {
        boolean removingSelf = false;
        List<String> users = new LinkedList<>();
        for (int i = 0, c = mMembersAdapter.getCount(); i < c; ++i) {
            if (checked.get(i)) {
                GroupMembersAdapter.GroupMember member = (GroupMembersAdapter.GroupMember) mMembersAdapter
                        .getItem(i);
                if (Authenticator.isSelfJID(getContext(), member.contact.getJID())) {
                    removingSelf = true;
                } else {
                    users.add(member.contact.getJID());
                }
            }
        }

        if (users.size() > 0) {
            mConversation.removeUsers(users.toArray(new String[users.size()]));
            reload();
        }

        if (removingSelf)
            confirmLeave();
    }

    @Override
    public void onContactInvalidated(String userId) {
        Activity context = getActivity();
        if (context != null) {
            context.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    // just reload
                    reload();
                }
            });
        }
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        int choiceMode = l.getChoiceMode();
        if (choiceMode == ListView.CHOICE_MODE_NONE || choiceMode == ListView.CHOICE_MODE_SINGLE) {
            // open identity dialog
            // one day this will be the contact info activity
            GroupMembersAdapter.GroupMember member = (GroupMembersAdapter.GroupMember) mMembersAdapter
                    .getItem(position);
            showIdentityDialog(member.contact, member.subscribed);
        } else {
            super.onListItemClick(l, v, position, id);
        }
    }

    private void showIdentityDialog(Contact c, boolean subscribed) {
        final String jid = c.getJID();
        final String dialogFingerprint;
        final String fingerprint;
        final boolean selfJid = Authenticator.isSelfJID(getContext(), jid);
        int titleResId = R.string.title_identity;
        String uid;

        PGPPublicKeyRing publicKey = Keyring.getPublicKey(getContext(), jid, MyUsers.Keys.TRUST_UNKNOWN);
        if (publicKey != null) {
            PGPPublicKey pk = PGP.getMasterKey(publicKey);
            String rawFingerprint = PGP.getFingerprint(pk);
            fingerprint = PGP.formatFingerprint(rawFingerprint);

            uid = PGP.getUserId(pk, XmppStringUtils.parseDomain(jid));
            dialogFingerprint = selfJid ? null : rawFingerprint;
        } else {
            // FIXME using another string
            fingerprint = getString(R.string.peer_unknown);
            uid = null;
            dialogFingerprint = null;
        }

        if (Authenticator.isSelfJID(getContext(), jid)) {
            titleResId = R.string.title_identity_self;
        }

        SpannableStringBuilder text = new SpannableStringBuilder();

        if (c.getName() != null && c.getNumber() != null) {
            text.append(c.getName()).append('\n').append(c.getNumber());
        } else {
            int start = text.length();
            text.append(uid != null ? uid : c.getJID());
            text.setSpan(SystemUtils.getTypefaceSpan(Typeface.BOLD), start, text.length(),
                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        text.append('\n').append(getString(R.string.text_invitation2)).append('\n');

        int start = text.length();
        text.append(fingerprint);
        text.setSpan(SystemUtils.getTypefaceSpan(Typeface.BOLD), start, text.length(),
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        int trustStringId;
        CharacterStyle[] trustSpans;

        if (subscribed) {
            int trustedLevel;
            if (c.isKeyChanged()) {
                // the key has changed and was not trusted yet
                trustedLevel = MyUsers.Keys.TRUST_UNKNOWN;
            } else {
                trustedLevel = c.getTrustedLevel();
            }

            switch (trustedLevel) {
            case MyUsers.Keys.TRUST_IGNORED:
                trustStringId = R.string.trust_ignored;
                trustSpans = new CharacterStyle[] { SystemUtils.getTypefaceSpan(Typeface.BOLD),
                        SystemUtils.getColoredSpan(getContext(), R.color.button_danger) };
                break;

            case MyUsers.Keys.TRUST_VERIFIED:
                trustStringId = R.string.trust_verified;
                trustSpans = new CharacterStyle[] { SystemUtils.getTypefaceSpan(Typeface.BOLD),
                        SystemUtils.getColoredSpan(getContext(), R.color.button_success) };
                break;

            case MyUsers.Keys.TRUST_UNKNOWN:
            default:
                trustStringId = R.string.trust_unknown;
                trustSpans = new CharacterStyle[] { SystemUtils.getTypefaceSpan(Typeface.BOLD),
                        SystemUtils.getColoredSpan(getContext(), R.color.button_danger) };
                break;
            }
        } else {
            trustStringId = R.string.status_notsubscribed;
            trustSpans = new CharacterStyle[] { SystemUtils.getTypefaceSpan(Typeface.BOLD), };
        }

        text.append('\n').append(getString(R.string.status_label));
        start = text.length();
        text.append(getString(trustStringId));
        for (CharacterStyle span : trustSpans)
            text.setSpan(span, start, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        MaterialDialog.Builder builder = new MaterialDialog.Builder(getContext()).content(text).title(titleResId);

        if (dialogFingerprint != null && subscribed) {
            builder.onAny(new MaterialDialog.SingleButtonCallback() {
                @Override
                public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                    switch (which) {
                    case POSITIVE:
                        // trust the key
                        trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_VERIFIED);
                        break;
                    case NEUTRAL:
                        // ignore the key
                        trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_IGNORED);
                        break;
                    case NEGATIVE:
                        // untrust the key
                        trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_UNKNOWN);
                        break;
                    }
                }
            }).positiveText(R.string.button_accept).positiveColorRes(R.color.button_success)
                    .neutralText(R.string.button_ignore).negativeText(R.string.button_refuse)
                    .negativeColorRes(R.color.button_danger);
        } else if (!selfJid) {
            builder.onPositive(new MaterialDialog.SingleButtonCallback() {
                @Override
                public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                    openChat(jid);
                }
            }).positiveText(R.string.button_private_chat);
        }

        builder.show();
    }

    private void readdUser(final SparseBooleanArray checked) {
        List<String> users = new LinkedList<>();
        for (int i = 0, c = mMembersAdapter.getCount(); i < c; ++i) {
            if (checked.get(i)) {
                GroupMembersAdapter.GroupMember member = (GroupMembersAdapter.GroupMember) mMembersAdapter
                        .getItem(i);
                if (!Authenticator.isSelfJID(getContext(), member.contact.getJID())) {
                    users.add(member.contact.getJID());
                }
            }
        }

        if (users.size() > 0) {
            mConversation.addUsers(users.toArray(new String[users.size()]));
        }

        getActivity().finish();
    }

    void openChat(String jid) {
        Intent i = new Intent();
        i.setData(MyMessages.Threads.getUri(jid));
        Activity parent = getActivity();
        parent.setResult(GroupInfoActivity.RESULT_PRIVATE_CHAT, i);
        parent.finish();
    }

    void trustKey(String jid, String fingerprint, int trustLevel) {
        Kontalk.getMessagesController(getContext()).setTrustLevelAndRetryMessages(getContext(), jid, fingerprint,
                trustLevel);
        Contact.invalidate(jid);
        reload();
    }

    void confirmLeave() {
        new MaterialDialog.Builder(getContext()).content(R.string.confirm_will_leave_group)
                .positiveText(android.R.string.ok).negativeText(android.R.string.cancel)
                .onPositive(new MaterialDialog.SingleButtonCallback() {
                    @Override
                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
                        // leave group
                        mConversation.leaveGroup();
                        getActivity().finish();
                    }
                }).show();
    }

    void reload() {
        // reload conversation data
        Bundle data = getArguments();
        long threadId = data.getLong("conversation");
        loadConversation(threadId);
    }

    @Override
    public void onResume() {
        super.onResume();
        reload();
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (!(context instanceof GroupInfoParent))
            throw new IllegalArgumentException(
                    "parent activity must implement " + GroupInfoParent.class.getSimpleName());
        mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        if (mLocalBroadcastManager != null)
            mLocalBroadcastManager.unregisterReceiver(mRosterReceiver);
    }

    private static final class GroupMembersAdapter extends BaseAdapter {
        private static final class GroupMember {
            final Contact contact;
            boolean subscribed;

            GroupMember(Contact contact, boolean subscribed) {
                this.contact = contact;
                this.subscribed = subscribed;
            }
        }

        private final Context mContext;
        private final List<GroupMember> mMembers;
        private String mOwner;
        private String mGroupJid;

        GroupMembersAdapter(Context context, String groupJid) {
            mContext = context;
            mMembers = new LinkedList<>();
            mGroupJid = groupJid;
        }

        public void setGroupJid(String groupJid) {
            mGroupJid = groupJid;
        }

        public void clear() {
            mMembers.clear();
        }

        @Override
        public void notifyDataSetChanged() {
            Collections.sort(mMembers, new DisplayNameComparator());
            super.notifyDataSetChanged();
        }

        public void add(Contact contact, boolean isOwner, boolean subscribed) {
            mMembers.add(new GroupMember(contact, subscribed));
            if (isOwner)
                mOwner = contact.getJID();
        }

        @Override
        public int getCount() {
            return mMembers.size();
        }

        @Override
        public Object getItem(int position) {
            return mMembers.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View v;
            if (convertView == null) {
                v = newView(parent);
            } else {
                v = convertView;
            }
            bindView(v, position);
            return v;
        }

        private View newView(ViewGroup parent) {
            return LayoutInflater.from(mContext).inflate(R.layout.contact_item, parent, false);
        }

        private void bindView(View v, int position) {
            ContactsListItem view = (ContactsListItem) v;
            GroupMember member = (GroupMember) getItem(position);
            Contact contact = member.contact;
            String prependStatus = null;
            CharacterStyle prependStyle = null;
            if (contact.getJID().equalsIgnoreCase(mOwner)) {
                prependStatus = mContext.getString(R.string.group_info_owner_member);
                prependStyle = new ForegroundColorSpan(Color.RED);
            }
            view.bind(mContext, contact, prependStatus, prependStyle, member.subscribed);
        }

        public void ignoreAll() {
            synchronized (mMembers) {
                for (GroupMember m : mMembers) {
                    Contact c = m.contact;
                    if (c.isKeyChanged() || c.getTrustedLevel() == MyUsers.Keys.TRUST_UNKNOWN) {
                        String fingerprint = c.getFingerprint();
                        Keyring.setTrustLevel(mContext, c.getJID(), fingerprint, MyUsers.Keys.TRUST_IGNORED);
                        Contact.invalidate(c.getJID());
                    }
                }
                MessageCenterService.retryMessagesTo(mContext, mGroupJid);
            }
        }

        public void setSubscribed(String jid, boolean isSubscribed) {
            synchronized (mMembers) {
                for (GroupMember m : mMembers) {
                    Contact c = m.contact;
                    if (c.getJID().equalsIgnoreCase(jid)) {
                        m.subscribed = isSubscribed;
                        break;
                    }
                }
            }
            // we don't need to sort, so call super directly
            super.notifyDataSetChanged();
        }

        static class DisplayNameComparator implements Comparator<GroupMember> {
            DisplayNameComparator() {
                mCollator.setStrength(Collator.PRIMARY);
            }

            public final int compare(GroupMember a, GroupMember b) {
                return mCollator.compare(a.contact.getDisplayName(), b.contact.getDisplayName());
            }

            private final Collator mCollator = Collator.getInstance();
        }

    }

    public interface GroupInfoParent {

        void dismiss();

    }

}