GroupEditorFragment.java revision 1c07d4bf219bcdcc433b69a40ed3398ba7b3389f
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.contacts.group;
18
19import com.android.contacts.ContactPhotoManager;
20import com.android.contacts.ContactSaveService;
21import com.android.contacts.GroupMemberLoader;
22import com.android.contacts.GroupMetaDataLoader;
23import com.android.contacts.R;
24import com.android.contacts.activities.GroupEditorActivity;
25import com.android.contacts.editor.ContactEditorFragment.SaveMode;
26import com.android.contacts.editor.SelectAccountDialogFragment;
27import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember;
28import com.android.contacts.model.AccountType;
29import com.android.contacts.model.AccountTypeManager;
30import com.android.contacts.model.AccountWithDataSet;
31import com.android.internal.util.Objects;
32
33import android.accounts.Account;
34import android.app.Activity;
35import android.app.AlertDialog;
36import android.app.Dialog;
37import android.app.DialogFragment;
38import android.app.Fragment;
39import android.app.LoaderManager;
40import android.app.LoaderManager.LoaderCallbacks;
41import android.content.ContentResolver;
42import android.content.ContentUris;
43import android.content.Context;
44import android.content.CursorLoader;
45import android.content.DialogInterface;
46import android.content.Intent;
47import android.content.Loader;
48import android.database.Cursor;
49import android.net.Uri;
50import android.os.Bundle;
51import android.provider.ContactsContract.Contacts;
52import android.provider.ContactsContract.Intents;
53import android.text.TextUtils;
54import android.util.Log;
55import android.view.LayoutInflater;
56import android.view.Menu;
57import android.view.MenuInflater;
58import android.view.MenuItem;
59import android.view.View;
60import android.view.View.OnClickListener;
61import android.view.ViewGroup;
62import android.widget.AdapterView;
63import android.widget.AdapterView.OnItemClickListener;
64import android.widget.AutoCompleteTextView;
65import android.widget.BaseAdapter;
66import android.widget.EditText;
67import android.widget.ImageView;
68import android.widget.ListView;
69import android.widget.QuickContactBadge;
70import android.widget.TextView;
71import android.widget.Toast;
72
73import java.util.ArrayList;
74import java.util.List;
75
76// TODO: Use savedInstanceState
77public class GroupEditorFragment extends Fragment implements SelectAccountDialogFragment.Listener {
78
79    private static final String TAG = "GroupEditorFragment";
80
81    private static final String LEGACY_CONTACTS_AUTHORITY = "contacts";
82
83    public static interface Listener {
84        /**
85         * Group metadata was not found, close the fragment now.
86         */
87        public void onGroupNotFound();
88
89        /**
90         * User has tapped Revert, close the fragment now.
91         */
92        void onReverted();
93
94        /**
95         * Title has been determined.
96         */
97        void onTitleLoaded(int resourceId);
98
99        /**
100         * Contact was saved and the Fragment can now be closed safely.
101         */
102        void onSaveFinished(int resultCode, Intent resultIntent);
103    }
104
105    private static final int LOADER_GROUP_METADATA = 1;
106    private static final int LOADER_EXISTING_MEMBERS = 2;
107    private static final int LOADER_NEW_GROUP_MEMBER = 3;
108
109    public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
110
111    private static final String MEMBER_RAW_CONTACT_ID_KEY = "rawContactId";
112    private static final String MEMBER_LOOKUP_URI_KEY = "memberLookupUri";
113
114    protected static final String[] PROJECTION_CONTACT = new String[] {
115        Contacts._ID,                           // 0
116        Contacts.DISPLAY_NAME_PRIMARY,          // 1
117        Contacts.DISPLAY_NAME_ALTERNATIVE,      // 2
118        Contacts.SORT_KEY_PRIMARY,              // 3
119        Contacts.STARRED,                       // 4
120        Contacts.CONTACT_PRESENCE,              // 5
121        Contacts.CONTACT_CHAT_CAPABILITY,       // 6
122        Contacts.PHOTO_ID,                      // 7
123        Contacts.PHOTO_THUMBNAIL_URI,           // 8
124        Contacts.LOOKUP_KEY,                    // 9
125        Contacts.PHONETIC_NAME,                 // 10
126        Contacts.HAS_PHONE_NUMBER,              // 11
127        Contacts.IS_USER_PROFILE,               // 12
128    };
129
130    protected static final int CONTACT_ID_COLUMN_INDEX = 0;
131    protected static final int CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
132    protected static final int CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
133    protected static final int CONTACT_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
134    protected static final int CONTACT_STARRED_COLUMN_INDEX = 4;
135    protected static final int CONTACT_PRESENCE_STATUS_COLUMN_INDEX = 5;
136    protected static final int CONTACT_CHAT_CAPABILITY_COLUMN_INDEX = 6;
137    protected static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 7;
138    protected static final int CONTACT_PHOTO_URI_COLUMN_INDEX = 8;
139    protected static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 9;
140    protected static final int CONTACT_PHONETIC_NAME_COLUMN_INDEX = 10;
141    protected static final int CONTACT_HAS_PHONE_COLUMN_INDEX = 11;
142    protected static final int CONTACT_IS_USER_PROFILE = 12;
143
144    /**
145     * Modes that specify the status of the editor
146     */
147    public enum Status {
148        LOADING,    // Loader is fetching the data
149        EDITING,    // Not currently busy. We are waiting forthe user to enter data.
150        SAVING,     // Data is currently being saved
151        CLOSING     // Prevents any more saves
152    }
153
154    private Context mContext;
155    private String mAction;
156    private Bundle mIntentExtras;
157    private Uri mGroupUri;
158    private long mGroupId;
159    private Listener mListener;
160
161    private Status mStatus;
162
163    private ViewGroup mRootView;
164    private ListView mListView;
165    private LayoutInflater mLayoutInflater;
166
167    private TextView mGroupNameView;
168    private ImageView mAccountIcon;
169    private TextView mAccountTypeTextView;
170    private TextView mAccountNameTextView;
171    private AutoCompleteTextView mAutoCompleteTextView;
172
173    private boolean mGroupNameIsReadOnly;
174    private String mAccountName;
175    private String mAccountType;
176    private String mDataSet;
177    private String mOriginalGroupName = "";
178
179    private MemberListAdapter mMemberListAdapter;
180    private ContactPhotoManager mPhotoManager;
181
182    private ContentResolver mContentResolver;
183    private SuggestedMemberListAdapter mAutoCompleteAdapter;
184
185    private List<Member> mListMembersToAdd = new ArrayList<Member>();
186    private List<Member> mListMembersToRemove = new ArrayList<Member>();
187    private List<Member> mListToDisplay = new ArrayList<Member>();
188
189    public GroupEditorFragment() {
190    }
191
192    @Override
193    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
194        setHasOptionsMenu(true);
195        mLayoutInflater = inflater;
196        mRootView = (ViewGroup) inflater.inflate(R.layout.group_editor_fragment, container, false);
197        return mRootView;
198    }
199
200    @Override
201    public void onAttach(Activity activity) {
202        super.onAttach(activity);
203        mContext = activity;
204        mPhotoManager = ContactPhotoManager.getInstance(mContext);
205        mMemberListAdapter = new MemberListAdapter();
206    }
207
208    @Override
209    public void onActivityCreated(Bundle savedInstanceState) {
210        super.onActivityCreated(savedInstanceState);
211
212        // Edit an existing group
213        if (Intent.ACTION_EDIT.equals(mAction)) {
214            if (mListener != null) {
215                mListener.onTitleLoaded(R.string.editGroup_title_edit);
216            }
217            getLoaderManager().initLoader(LOADER_GROUP_METADATA, null,
218                    mGroupMetaDataLoaderListener);
219        } else if (Intent.ACTION_INSERT.equals(mAction)) {
220            if (mListener != null) {
221                mListener.onTitleLoaded(R.string.editGroup_title_insert);
222            }
223
224            final Account account = mIntentExtras == null ? null :
225                    (Account) mIntentExtras.getParcelable(Intents.Insert.ACCOUNT);
226            final String dataSet = mIntentExtras == null ? null :
227                    mIntentExtras.getString(Intents.Insert.DATA_SET);
228
229            if (account != null) {
230                // Account specified in Intent - no data set can be specified in this manner.
231                mAccountName = account.name;
232                mAccountType = account.type;
233                mDataSet = dataSet;
234                setupEditorForAccount();
235            } else {
236                // No Account specified. Let the user choose from a disambiguation dialog.
237                selectAccountAndCreateGroup();
238            }
239        } else {
240            throw new IllegalArgumentException("Unknown Action String " + mAction +
241                    ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
242        }
243    }
244
245    public void setContentResolver(ContentResolver resolver) {
246        mContentResolver = resolver;
247        if (mAutoCompleteAdapter != null) {
248            mAutoCompleteAdapter.setContentResolver(mContentResolver);
249        }
250    }
251
252    private void selectAccountAndCreateGroup() {
253        final List<AccountWithDataSet> accounts =
254                AccountTypeManager.getInstance(mContext).getAccounts(true /* writeable */);
255        // No Accounts available
256        if (accounts.isEmpty()) {
257            throw new IllegalStateException("No accounts were found.");
258        }
259
260        // In the common case of a single account being writable, auto-select
261        // it without showing a dialog.
262        if (accounts.size() == 1) {
263            mAccountName = accounts.get(0).name;
264            mAccountType = accounts.get(0).type;
265            mDataSet = accounts.get(0).dataSet;
266            setupEditorForAccount();
267            return;  // Don't show a dialog.
268        }
269
270        final SelectAccountDialogFragment dialog = new SelectAccountDialogFragment(
271                R.string.dialog_new_group_account);
272        dialog.setTargetFragment(this, 0);
273        dialog.show(getFragmentManager(), SelectAccountDialogFragment.TAG);
274    }
275
276    @Override
277    public void onAccountChosen(int requestCode, AccountWithDataSet account) {
278        mAccountName = account.name;
279        mAccountType = account.type;
280        mDataSet = account.dataSet;
281        setupEditorForAccount();
282    }
283
284    @Override
285    public void onAccountSelectorCancelled() {
286        if (mListener != null) {
287            // Exit the fragment because we cannot continue without selecting an account
288            mListener.onGroupNotFound();
289        }
290    }
291
292    /**
293     * Sets up the editor based on the group's account name and type.
294     */
295    private void setupEditorForAccount() {
296        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
297        final AccountType accountType = accountTypeManager.getAccountType(mAccountType, mDataSet);
298        final boolean editable = accountType.isGroupMembershipEditable();
299        mMemberListAdapter.setIsGroupMembershipEditable(editable);
300
301        View editorView = mLayoutInflater.inflate(editable ?
302                R.layout.group_editor_view : R.layout.external_group_editor_view, mRootView, false);
303
304        mGroupNameView = (TextView) editorView.findViewById(R.id.group_name);
305        mAccountIcon = (ImageView) editorView.findViewById(R.id.account_icon);
306        mAccountTypeTextView = (TextView) editorView.findViewById(R.id.account_type);
307        mAccountNameTextView = (TextView) editorView.findViewById(R.id.account_name);
308        mAutoCompleteTextView = (AutoCompleteTextView) editorView.findViewById(
309                R.id.add_member_field);
310
311        mListView = (ListView) editorView.findViewById(android.R.id.list);
312        mListView.setAdapter(mMemberListAdapter);
313
314        // Setup the account header
315        CharSequence accountTypeDisplayLabel = accountType.getDisplayLabel(mContext);
316        if (!TextUtils.isEmpty(mAccountName)) {
317            mAccountNameTextView.setText(
318                    mContext.getString(R.string.from_account_format, mAccountName));
319        }
320        mAccountTypeTextView.setText(accountTypeDisplayLabel);
321        mAccountIcon.setImageDrawable(accountType.getDisplayIcon(mContext));
322
323        // Setup the autocomplete adapter (for contacts to suggest to add to the group) based on the
324        // account name and type. For groups that cannot have membership edited, there will be no
325        // autocomplete text view.
326        if (mAutoCompleteTextView != null) {
327            mAutoCompleteAdapter = new SuggestedMemberListAdapter(mContext,
328                    android.R.layout.simple_dropdown_item_1line);
329            mAutoCompleteAdapter.setContentResolver(mContentResolver);
330            mAutoCompleteAdapter.setAccountType(mAccountType);
331            mAutoCompleteAdapter.setAccountName(mAccountName);
332            mAutoCompleteAdapter.setDataSet(mDataSet);
333            mAutoCompleteTextView.setAdapter(mAutoCompleteAdapter);
334            mAutoCompleteTextView.setOnItemClickListener(new OnItemClickListener() {
335                @Override
336                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
337                    SuggestedMember member = mAutoCompleteAdapter.getItem(position);
338                    loadMemberToAddToGroup(member.getRawContactId(),
339                            String.valueOf(member.getContactId()));
340
341                    // Update the autocomplete adapter so the contact doesn't get suggested again
342                    mAutoCompleteAdapter.addNewMember(member.getContactId());
343
344                    // Clear out the text field
345                    mAutoCompleteTextView.setText("");
346                }
347            });
348        }
349
350        mRootView.addView(editorView);
351        mStatus = Status.EDITING;
352    }
353
354    public void load(String action, Uri groupUri, Bundle intentExtras) {
355        mAction = action;
356        mGroupUri = groupUri;
357        mGroupId = (groupUri != null) ? ContentUris.parseId(mGroupUri) : 0;
358        mIntentExtras = intentExtras;
359    }
360
361    private void bindGroupMetaData(Cursor cursor) {
362        if (cursor.getCount() == 0) {
363            if (mListener != null) {
364                mListener.onGroupNotFound();
365            }
366        }
367        try {
368            cursor.moveToFirst();
369            mOriginalGroupName = cursor.getString(GroupMetaDataLoader.TITLE);
370            mAccountName = cursor.getString(GroupMetaDataLoader.ACCOUNT_NAME);
371            mAccountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
372            mGroupNameIsReadOnly = (cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1);
373        } catch (Exception e) {
374            Log.i(TAG, "Group not found with URI: " + mGroupUri + " Closing activity now.");
375            if (mListener != null) {
376                mListener.onGroupNotFound();
377            }
378        } finally {
379            cursor.close();
380        }
381        setupEditorForAccount();
382        // Setup the group metadata display (If the group name is ready only, don't let the user
383        // focus on the field).
384        mGroupNameView.setText(mOriginalGroupName);
385        mGroupNameView.setFocusable(!mGroupNameIsReadOnly);
386    }
387
388    public void loadMemberToAddToGroup(long rawContactId, String contactId) {
389        Bundle args = new Bundle();
390        args.putLong(MEMBER_RAW_CONTACT_ID_KEY, rawContactId);
391        args.putString(MEMBER_LOOKUP_URI_KEY, contactId);
392        getLoaderManager().restartLoader(LOADER_NEW_GROUP_MEMBER, args, mContactLoaderListener);
393    }
394
395    public void setListener(Listener value) {
396        mListener = value;
397    }
398
399    public void doSaveAction() {
400        save(SaveMode.CLOSE);
401    }
402
403    @Override
404    public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
405        inflater.inflate(R.menu.edit_group, menu);
406    }
407
408    @Override
409    public boolean onOptionsItemSelected(MenuItem item) {
410        switch (item.getItemId()) {
411            case R.id.menu_discard:
412                return revert();
413        }
414        return false;
415    }
416
417    private boolean revert() {
418        if (!hasNameChange() && !hasMembershipChange()) {
419            doRevertAction();
420        } else {
421            CancelEditDialogFragment.show(this);
422        }
423        return true;
424    }
425
426    private void doRevertAction() {
427        // When this Fragment is closed we don't want it to auto-save
428        mStatus = Status.CLOSING;
429        if (mListener != null) mListener.onReverted();
430    }
431
432    public static class CancelEditDialogFragment extends DialogFragment {
433
434        public static void show(GroupEditorFragment fragment) {
435            CancelEditDialogFragment dialog = new CancelEditDialogFragment();
436            dialog.setTargetFragment(fragment, 0);
437            dialog.show(fragment.getFragmentManager(), "cancelEditor");
438        }
439
440        @Override
441        public Dialog onCreateDialog(Bundle savedInstanceState) {
442            AlertDialog dialog = new AlertDialog.Builder(getActivity())
443                    .setIconAttribute(android.R.attr.alertDialogIcon)
444                    .setTitle(R.string.cancel_confirmation_dialog_title)
445                    .setMessage(R.string.cancel_confirmation_dialog_message)
446                    .setPositiveButton(R.string.discard,
447                        new DialogInterface.OnClickListener() {
448                            @Override
449                            public void onClick(DialogInterface dialog, int whichButton) {
450                                ((GroupEditorFragment) getTargetFragment()).doRevertAction();
451                            }
452                        }
453                    )
454                    .setNegativeButton(android.R.string.cancel, null)
455                    .create();
456            return dialog;
457        }
458    }
459
460    /**
461     * Saves or creates the group based on the mode, and if successful
462     * finishes the activity. This actually only handles saving the group name.
463     * @return true when successful
464     */
465    public boolean save(int saveMode) {
466        if (!hasValidGroupName() || mStatus != Status.EDITING) {
467            return false;
468        }
469
470        // If we are about to close the editor - there is no need to refresh the data
471        if (saveMode == SaveMode.CLOSE) {
472            getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS);
473        }
474
475        // If there are no changes, then go straight to onSaveCompleted()
476        if (!hasNameChange() && !hasMembershipChange()) {
477            onSaveCompleted(false, SaveMode.CLOSE, mGroupUri);
478            return true;
479        }
480
481        mStatus = Status.SAVING;
482
483        Activity activity = getActivity();
484        // If the activity is not there anymore, then we can't continue with the save process.
485        if (activity == null) {
486            return false;
487        }
488        Intent saveIntent = null;
489        if (Intent.ACTION_INSERT.equals(mAction)) {
490            // Create array of raw contact IDs for contacts to add to the group
491            long[] membersToAddArray = convertToArray(mListMembersToAdd);
492
493            // Create the save intent to create the group and add members at the same time
494            saveIntent = ContactSaveService.createNewGroupIntent(activity,
495                    new AccountWithDataSet(mAccountName, mAccountType, mDataSet),
496                    mGroupNameView.getText().toString(),
497                    membersToAddArray, activity.getClass(),
498                    GroupEditorActivity.ACTION_SAVE_COMPLETED);
499        } else if (Intent.ACTION_EDIT.equals(mAction)) {
500            // Create array of raw contact IDs for contacts to add to the group
501            long[] membersToAddArray = convertToArray(mListMembersToAdd);
502
503            // Create array of raw contact IDs for contacts to add to the group
504            long[] membersToRemoveArray = convertToArray(mListMembersToRemove);
505
506            // Create the update intent (which includes the updated group name if necessary)
507            saveIntent = ContactSaveService.createGroupUpdateIntent(activity, mGroupId,
508                    getUpdatedName(), membersToAddArray, membersToRemoveArray,
509                    activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED);
510        } else {
511            throw new IllegalStateException("Invalid intent action type " + mAction);
512        }
513        activity.startService(saveIntent);
514        return true;
515    }
516
517    public void onSaveCompleted(boolean hadChanges, int saveMode, Uri groupUri) {
518        boolean success = groupUri != null;
519        Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + groupUri + ")");
520        if (hadChanges) {
521            Toast.makeText(mContext, success ? R.string.groupSavedToast :
522                    R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show();
523        }
524        switch (saveMode) {
525            case SaveMode.CLOSE:
526            case SaveMode.HOME:
527                final Intent resultIntent;
528                final int resultCode;
529                if (success && groupUri != null) {
530                    final String requestAuthority =
531                            groupUri == null ? null : groupUri.getAuthority();
532
533                    resultIntent = new Intent();
534                    if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) {
535                        // Build legacy Uri when requested by caller
536                        final long groupId = ContentUris.parseId(groupUri);
537                        final Uri legacyContentUri = Uri.parse("content://contacts/groups");
538                        final Uri legacyUri = ContentUris.withAppendedId(
539                                legacyContentUri, groupId);
540                        resultIntent.setData(legacyUri);
541                    } else {
542                        // Otherwise pass back the given Uri
543                        resultIntent.setData(groupUri);
544                    }
545
546                    resultCode = Activity.RESULT_OK;
547                } else {
548                    resultCode = Activity.RESULT_CANCELED;
549                    resultIntent = null;
550                }
551                // It is already saved, so prevent that it is saved again
552                mStatus = Status.CLOSING;
553                if (mListener != null) {
554                    mListener.onSaveFinished(resultCode, resultIntent);
555                }
556                break;
557            case SaveMode.RELOAD:
558                // TODO: Handle reloading the group list
559            default:
560                throw new IllegalStateException("Unsupported save mode " + saveMode);
561        }
562    }
563
564    private boolean hasValidGroupName() {
565        return mGroupNameView != null && !TextUtils.isEmpty(mGroupNameView.getText());
566    }
567
568    private boolean hasNameChange() {
569        return mGroupNameView != null &&
570                !mGroupNameView.getText().toString().equals(mOriginalGroupName);
571    }
572
573    private boolean hasMembershipChange() {
574        return mListMembersToAdd.size() > 0 || mListMembersToRemove.size() > 0;
575    }
576
577    /**
578     * Returns the group's new name or null if there is no change from the
579     * original name that was loaded for the group.
580     */
581    private String getUpdatedName() {
582        String groupNameFromTextView = mGroupNameView.getText().toString();
583        if (groupNameFromTextView.equals(mOriginalGroupName)) {
584            // No name change, so return null
585            return null;
586        }
587        return groupNameFromTextView;
588    }
589
590    private static long[] convertToArray(List<Member> listMembers) {
591        int size = listMembers.size();
592        long[] membersArray = new long[size];
593        for (int i = 0; i < size; i++) {
594            membersArray[i] = listMembers.get(i).getRawContactId();
595        }
596        return membersArray;
597    }
598
599    private void addExistingMembers(List<Member> members, List<Long> listContactIds) {
600        mListToDisplay.addAll(members);
601        mMemberListAdapter.notifyDataSetChanged();
602
603        // Update the autocomplete adapter (if there is one) so these contacts don't get suggested
604        if (mAutoCompleteAdapter != null) {
605            mAutoCompleteAdapter.updateExistingMembersList(listContactIds);
606        }
607    }
608
609    private void addMember(Member member) {
610        // Update the display list
611        mListMembersToAdd.add(member);
612        mListToDisplay.add(member);
613        mMemberListAdapter.notifyDataSetChanged();
614
615        // Update the autocomplete adapter so the contact doesn't get suggested again
616        mAutoCompleteAdapter.addNewMember(member.getContactId());
617    }
618
619    private void removeMember(Member member) {
620        // If the contact was just added during this session, remove it from the list of
621        // members to add
622        if (mListMembersToAdd.contains(member)) {
623            mListMembersToAdd.remove(member);
624        } else {
625            // Otherwise this contact was already part of the existing list of contacts,
626            // so we need to do a content provider deletion operation
627            mListMembersToRemove.add(member);
628        }
629        // In either case, update the UI so the contact is no longer in the list of
630        // members
631        mListToDisplay.remove(member);
632        mMemberListAdapter.notifyDataSetChanged();
633
634        // Update the autocomplete adapter so the contact can get suggested again
635        mAutoCompleteAdapter.removeMember(member.getContactId());
636    }
637
638    /**
639     * The listener for the group metadata (i.e. group name, account type, and account name) loader.
640     */
641    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener =
642            new LoaderCallbacks<Cursor>() {
643
644        @Override
645        public CursorLoader onCreateLoader(int id, Bundle args) {
646            return new GroupMetaDataLoader(mContext, mGroupUri);
647        }
648
649        @Override
650        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
651            bindGroupMetaData(data);
652
653            // Load existing members
654            getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null,
655                    mGroupMemberListLoaderListener);
656        }
657
658        @Override
659        public void onLoaderReset(Loader<Cursor> loader) {}
660    };
661
662    /**
663     * The loader listener for the list of existing group members.
664     */
665    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener =
666            new LoaderCallbacks<Cursor>() {
667
668        @Override
669        public CursorLoader onCreateLoader(int id, Bundle args) {
670            return new GroupMemberLoader(mContext, mGroupId);
671        }
672
673        @Override
674        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
675            List<Long> listContactIds = new ArrayList<Long>();
676            List<Member> listExistingMembers = new ArrayList<Member>();
677            try {
678                data.moveToPosition(-1);
679                while (data.moveToNext()) {
680                    long contactId = data.getLong(GroupMemberLoader.CONTACT_ID_COLUMN_INDEX);
681                    long rawContactId = data.getLong(GroupMemberLoader.RAW_CONTACT_ID_COLUMN_INDEX);
682                    String lookupKey = data.getString(
683                            GroupMemberLoader.CONTACT_LOOKUP_KEY_COLUMN_INDEX);
684                    String displayName = data.getString(
685                            GroupMemberLoader.CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
686                    String photoUri = data.getString(
687                            GroupMemberLoader.CONTACT_PHOTO_URI_COLUMN_INDEX);
688                    listExistingMembers.add(new Member(rawContactId, lookupKey, contactId,
689                            displayName, photoUri));
690                    listContactIds.add(contactId);
691                }
692            } finally {
693                data.close();
694            }
695
696            // Update the display list
697            addExistingMembers(listExistingMembers, listContactIds);
698
699            // No more updates
700            // TODO: move to a runnable
701            getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS);
702        }
703
704        @Override
705        public void onLoaderReset(Loader<Cursor> loader) {}
706    };
707
708    /**
709     * The listener to load a summary of details for a contact.
710     */
711    // TODO: Remove this step because showing the aggregate contact can be confusing when the user
712    // just selected a raw contact
713    private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener =
714            new LoaderCallbacks<Cursor>() {
715
716        private long mRawContactId;
717
718        @Override
719        public CursorLoader onCreateLoader(int id, Bundle args) {
720            String memberId = args.getString(MEMBER_LOOKUP_URI_KEY);
721            mRawContactId = args.getLong(MEMBER_RAW_CONTACT_ID_KEY);
722            return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId),
723                    PROJECTION_CONTACT, null, null, null);
724        }
725
726        @Override
727        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
728            // Retrieve the contact data fields that will be sufficient to update the adapter with
729            // a new entry for this contact
730            Member member = null;
731            try {
732                cursor.moveToFirst();
733                long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
734                String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
735                String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
736                String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX);
737                getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER);
738                member = new Member(mRawContactId, lookupKey, contactId, displayName, photoUri);
739            } finally {
740                cursor.close();
741            }
742
743            if (member == null) {
744                return;
745            }
746
747            // Otherwise continue adding the member to list of members
748            addMember(member);
749        }
750
751        @Override
752        public void onLoaderReset(Loader<Cursor> loader) {}
753    };
754
755    /**
756     * This represents a single member of the current group.
757     */
758    public static class Member {
759        // TODO: Switch to just dealing with raw contact IDs everywhere if possible
760        private final long mRawContactId;
761        private final long mContactId;
762        private final Uri mLookupUri;
763        private final String mDisplayName;
764        private final Uri mPhotoUri;
765
766        public Member(long rawContactId, String lookupKey, long contactId, String displayName,
767                String photoUri) {
768            mRawContactId = rawContactId;
769            mContactId = contactId;
770            mLookupUri = Contacts.getLookupUri(contactId, lookupKey);
771            mDisplayName = displayName;
772            mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null;
773        }
774
775        public long getRawContactId() {
776            return mRawContactId;
777        }
778
779        public long getContactId() {
780            return mContactId;
781        }
782
783        public Uri getLookupUri() {
784            return mLookupUri;
785        }
786
787        public String getDisplayName() {
788            return mDisplayName;
789        }
790
791        public Uri getPhotoUri() {
792            return mPhotoUri;
793        }
794
795        @Override
796        public boolean equals(Object object) {
797            if (object instanceof Member) {
798                Member otherMember = (Member) object;
799                return otherMember != null && Objects.equal(mLookupUri, otherMember.getLookupUri());
800            }
801            return false;
802        }
803    }
804
805    /**
806     * This adapter displays a list of members for the current group being edited.
807     */
808    private final class MemberListAdapter extends BaseAdapter {
809
810        private boolean mIsGroupMembershipEditable = true;
811
812        @Override
813        public View getView(int position, View convertView, ViewGroup parent) {
814            View result;
815            if (convertView == null) {
816                result = mLayoutInflater.inflate(mIsGroupMembershipEditable ?
817                        R.layout.group_member_item : R.layout.external_group_member_item,
818                        parent, false);
819            } else {
820                result = convertView;
821            }
822            final Member member = getItem(position);
823
824            QuickContactBadge badge = (QuickContactBadge) result.findViewById(R.id.badge);
825            badge.assignContactUri(member.getLookupUri());
826
827            TextView name = (TextView) result.findViewById(R.id.name);
828            name.setText(member.getDisplayName());
829
830            View deleteButton = result.findViewById(R.id.delete_button_container);
831            if (deleteButton != null) {
832                deleteButton.setOnClickListener(new OnClickListener() {
833                    @Override
834                    public void onClick(View v) {
835                        removeMember(member);
836                    }
837                });
838            }
839
840            mPhotoManager.loadPhoto(badge, member.getPhotoUri());
841            return result;
842        }
843
844        @Override
845        public int getCount() {
846            return mListToDisplay.size();
847        }
848
849        @Override
850        public Member getItem(int position) {
851            return mListToDisplay.get(position);
852        }
853
854        @Override
855        public long getItemId(int position) {
856            return position;
857        }
858
859        public void setIsGroupMembershipEditable(boolean editable) {
860            mIsGroupMembershipEditable = editable;
861        }
862    }
863}
864