GroupEditorFragment.java revision c6b8afe730255537978f2c938cca6986cae63c34
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.ContactLoader;
20import com.android.contacts.ContactPhotoManager;
21import com.android.contacts.ContactSaveService;
22import com.android.contacts.GroupMemberLoader;
23import com.android.contacts.GroupMetaDataLoader;
24import com.android.contacts.R;
25import com.android.contacts.activities.GroupEditorActivity;
26import com.android.contacts.editor.ContactEditorFragment.SaveMode;
27import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember;
28import com.android.contacts.model.AccountType;
29import com.android.contacts.model.AccountTypeManager;
30import com.android.contacts.model.DataKind;
31import com.android.contacts.model.EntityDelta;
32import com.android.contacts.model.EntityDelta.ValuesDelta;
33import com.android.contacts.model.EntityDeltaList;
34import com.android.contacts.model.EntityModifier;
35import com.android.internal.util.Objects;
36
37import android.accounts.Account;
38import android.app.Activity;
39import android.app.AlertDialog;
40import android.app.Dialog;
41import android.app.DialogFragment;
42import android.app.Fragment;
43import android.app.LoaderManager;
44import android.app.LoaderManager.LoaderCallbacks;
45import android.content.ContentResolver;
46import android.content.ContentUris;
47import android.content.Context;
48import android.content.CursorLoader;
49import android.content.DialogInterface;
50import android.content.Intent;
51import android.content.Loader;
52import android.database.Cursor;
53import android.net.Uri;
54import android.os.Bundle;
55import android.provider.ContactsContract.Contacts;
56import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
57import android.provider.ContactsContract.RawContacts;
58import android.text.TextUtils;
59import android.util.Log;
60import android.view.LayoutInflater;
61import android.view.Menu;
62import android.view.MenuInflater;
63import android.view.MenuItem;
64import android.view.View;
65import android.view.View.OnClickListener;
66import android.view.ViewGroup;
67import android.widget.AdapterView;
68import android.widget.AdapterView.OnItemClickListener;
69import android.widget.AutoCompleteTextView;
70import android.widget.BaseAdapter;
71import android.widget.EditText;
72import android.widget.ImageView;
73import android.widget.ListView;
74import android.widget.QuickContactBadge;
75import android.widget.TextView;
76import android.widget.Toast;
77
78import java.util.ArrayList;
79import java.util.List;
80
81// TODO: Use savedInstanceState
82public class GroupEditorFragment extends Fragment {
83
84    private static final String TAG = "GroupEditorFragment";
85
86    private static final String LEGACY_CONTACTS_AUTHORITY = "contacts";
87
88    public static interface Listener {
89        /**
90         * Group metadata was not found, close the fragment now.
91         */
92        public void onGroupNotFound();
93
94        /**
95         * User has tapped Revert, close the fragment now.
96         */
97        void onReverted();
98
99        /**
100         * Title has been determined.
101         */
102        void onTitleLoaded(int resourceId);
103
104        /**
105         * Contact was saved and the Fragment can now be closed safely.
106         */
107        void onSaveFinished(int resultCode, Intent resultIntent, boolean navigateHome);
108    }
109
110    private static final int LOADER_GROUP_METADATA = 1;
111    private static final int LOADER_EXISTING_MEMBERS = 2;
112    private static final int LOADER_NEW_GROUP_MEMBER = 3;
113    private static final int FULL_LOADER_NEW_GROUP_MEMBER = 4;
114
115    public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
116
117    private static final String MEMBER_LOOKUP_URI_KEY = "memberLookupUri";
118    private static final String MEMBER_ACTION_KEY = "memberAction";
119
120    private static final int ADD_MEMBER = 0;
121    private static final int REMOVE_MEMBER = 1;
122
123    protected static final String[] PROJECTION_CONTACT = new String[] {
124        Contacts._ID,                           // 0
125        Contacts.DISPLAY_NAME_PRIMARY,          // 1
126        Contacts.DISPLAY_NAME_ALTERNATIVE,      // 2
127        Contacts.SORT_KEY_PRIMARY,              // 3
128        Contacts.STARRED,                       // 4
129        Contacts.CONTACT_PRESENCE,              // 5
130        Contacts.CONTACT_CHAT_CAPABILITY,       // 6
131        Contacts.PHOTO_ID,                      // 7
132        Contacts.PHOTO_THUMBNAIL_URI,           // 8
133        Contacts.LOOKUP_KEY,                    // 9
134        Contacts.PHONETIC_NAME,                 // 10
135        Contacts.HAS_PHONE_NUMBER,              // 11
136        Contacts.IS_USER_PROFILE,               // 12
137    };
138
139    protected static final int CONTACT_ID_COLUMN_INDEX = 0;
140    protected static final int CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
141    protected static final int CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
142    protected static final int CONTACT_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
143    protected static final int CONTACT_STARRED_COLUMN_INDEX = 4;
144    protected static final int CONTACT_PRESENCE_STATUS_COLUMN_INDEX = 5;
145    protected static final int CONTACT_CHAT_CAPABILITY_COLUMN_INDEX = 6;
146    protected static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 7;
147    protected static final int CONTACT_PHOTO_URI_COLUMN_INDEX = 8;
148    protected static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 9;
149    protected static final int CONTACT_PHONETIC_NAME_COLUMN_INDEX = 10;
150    protected static final int CONTACT_HAS_PHONE_COLUMN_INDEX = 11;
151    protected static final int CONTACT_IS_USER_PROFILE = 12;
152
153    /**
154     * Modes that specify the status of the editor
155     */
156    public enum Status {
157        LOADING,    // Loader is fetching the data
158        EDITING,    // Not currently busy. We are waiting forthe user to enter data.
159        SAVING,     // Data is currently being saved
160        CLOSING     // Prevents any more saves
161    }
162
163    private Context mContext;
164    private String mAction;
165    private Uri mGroupUri;
166    private long mGroupId;
167    private Listener mListener;
168
169    private Status mStatus;
170
171    private View mRootView;
172    private ListView mListView;
173    private LayoutInflater mLayoutInflater;
174
175    private EditText mGroupNameView;
176    private ImageView mAccountIcon;
177    private TextView mAccountTypeTextView;
178    private TextView mAccountNameTextView;
179    private AutoCompleteTextView mAutoCompleteTextView;
180
181    private boolean mGroupNameIsReadOnly;
182    private String mAccountName;
183    private String mAccountType;
184    private String mOriginalGroupName = "";
185
186    private MemberListAdapter mMemberListAdapter;
187    private ContactPhotoManager mPhotoManager;
188
189    private Member mMemberToRemove;
190
191    private ContentResolver mContentResolver;
192    private SuggestedMemberListAdapter mAutoCompleteAdapter;
193
194    public GroupEditorFragment() {
195    }
196
197    @Override
198    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
199        setHasOptionsMenu(true);
200
201        mLayoutInflater = inflater;
202        mRootView = inflater.inflate(R.layout.group_editor_fragment, container, false);
203
204        mGroupNameView = (EditText) mRootView.findViewById(R.id.group_name);
205        mAccountIcon = (ImageView) mRootView.findViewById(R.id.account_icon);
206        mAccountTypeTextView = (TextView) mRootView.findViewById(R.id.account_type);
207        mAccountNameTextView = (TextView) mRootView.findViewById(R.id.account_name);
208        mAutoCompleteTextView = (AutoCompleteTextView) mRootView.findViewById(
209                R.id.add_member_field);
210
211        mListView = (ListView) mRootView.findViewById(android.R.id.list);
212        mListView.setAdapter(mMemberListAdapter);
213
214        return mRootView;
215    }
216
217    @Override
218    public void onAttach(Activity activity) {
219        super.onAttach(activity);
220        mContext = activity;
221        mPhotoManager = ContactPhotoManager.getInstance(mContext);
222        mMemberListAdapter = new MemberListAdapter();
223    }
224
225    @Override
226    public void onActivityCreated(Bundle savedInstanceState) {
227        super.onActivityCreated(savedInstanceState);
228
229        // Edit an existing group
230        if (Intent.ACTION_EDIT.equals(mAction)) {
231            if (mListener != null) {
232                mListener.onTitleLoaded(R.string.editGroup_title_edit);
233            }
234            getLoaderManager().initLoader(LOADER_GROUP_METADATA, null,
235                    mGroupMetaDataLoaderListener);
236            getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null,
237                    mGroupMemberListLoaderListener);
238        } else if (Intent.ACTION_INSERT.equals(mAction)) {
239            if (mListener != null) {
240                mListener.onTitleLoaded(R.string.editGroup_title_insert);
241            }
242            setupAccountSwitcher();
243            mStatus = Status.EDITING;
244            // The user wants to create a new group, temporarily hide the "add members" text view
245            // TODO: Need to allow users to add members if it's a new group. Under the current
246            // approach, we can't add members because it needs a group ID in order to save,
247            // and we don't have a group ID for a new group until the whole group is saved.
248            mAutoCompleteTextView.setVisibility(View.GONE);
249        } else {
250            throw new IllegalArgumentException("Unknown Action String " + mAction +
251                    ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
252        }
253    }
254
255    public void setContentResolver(ContentResolver resolver) {
256        mContentResolver = resolver;
257        if (mAutoCompleteAdapter != null) {
258            mAutoCompleteAdapter.setContentResolver(mContentResolver);
259        }
260    }
261
262    /**
263     * Sets up the account header for a new group by taking the first account.
264     */
265    private void setupAccountSwitcher() {
266        // TODO: Allow switching between valid accounts
267        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
268        final ArrayList<Account> accountsList = accountTypeManager.getAccounts(true);
269        if (accountsList.isEmpty()) {
270            return;
271        }
272        Account account = accountsList.get(0);
273
274        // Store account info for later
275        mAccountName = account.name;
276        mAccountType = account.type;
277
278        // Display account name
279        if (!TextUtils.isEmpty(mAccountName)) {
280            mAccountNameTextView.setText(
281                    mContext.getString(R.string.from_account_format, mAccountName));
282        }
283        // Display account type
284        final AccountType type = accountTypeManager.getAccountType(mAccountType);
285        mAccountTypeTextView.setText(type.getDisplayLabel(mContext));
286
287        // Display account icon
288        mAccountIcon.setImageDrawable(type.getDisplayIcon(mContext));
289    }
290
291    /**
292     * Sets up the account header for an existing group.
293     */
294    private void setupAccountHeader() {
295        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
296        final AccountType accountType = accountTypes.getAccountType(mAccountType);
297        CharSequence accountTypeDisplayLabel = accountType.getDisplayLabel(mContext);
298        if (!TextUtils.isEmpty(mAccountName)) {
299            mAccountNameTextView.setText(
300                    mContext.getString(R.string.from_account_format, mAccountName));
301        }
302        mAccountTypeTextView.setText(accountTypeDisplayLabel);
303        mAccountIcon.setImageDrawable(accountType.getDisplayIcon(mContext));
304    }
305
306    public void load(String action, Uri groupUri) {
307        mAction = action;
308        mGroupUri = groupUri;
309        mGroupId = (groupUri != null) ? ContentUris.parseId(mGroupUri) : 0;
310    }
311
312    private void bindGroupMetaData(Cursor cursor) {
313        if (cursor.getCount() == 0) {
314            if (mListener != null) {
315                mListener.onGroupNotFound();
316            }
317        }
318        try {
319            cursor.moveToFirst();
320            mOriginalGroupName = cursor.getString(GroupMetaDataLoader.TITLE);
321            mAccountName = cursor.getString(GroupMetaDataLoader.ACCOUNT_NAME);
322            mAccountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
323            mGroupNameIsReadOnly = (cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1);
324        } catch (Exception e) {
325            Log.i(TAG, "Group not found with URI: " + mGroupUri + " Closing activity now.");
326            if (mListener != null) {
327                mListener.onGroupNotFound();
328            }
329        } finally {
330            cursor.close();
331        }
332        // Setup the group metadata display (If the group name is ready only, don't let the user
333        // focus on the field).
334        mGroupNameView.setText(mOriginalGroupName);
335        mGroupNameView.setFocusable(!mGroupNameIsReadOnly);
336        setupAccountHeader();
337
338        // Setup the group member suggestion adapter
339        mAutoCompleteAdapter = new SuggestedMemberListAdapter(getActivity(),
340                android.R.layout.simple_dropdown_item_1line);
341        mAutoCompleteAdapter.setContentResolver(mContentResolver);
342        mAutoCompleteAdapter.setAccountType(mAccountType);
343        mAutoCompleteAdapter.setAccountName(mAccountName);
344        mAutoCompleteTextView.setAdapter(mAutoCompleteAdapter);
345        mAutoCompleteTextView.setOnItemClickListener(new OnItemClickListener() {
346            @Override
347            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
348                SuggestedMember member = mAutoCompleteAdapter.getItem(position);
349                loadMemberToAddToGroup(String.valueOf(member.getContactId()));
350
351                // Update the autocomplete adapter so the contact doesn't get suggested again
352                mAutoCompleteAdapter.addNewMember(member.getContactId());
353
354                // Clear out the text field
355                mAutoCompleteTextView.setText("");
356            }
357        });
358    }
359
360    public void loadMemberToAddToGroup(String contactId) {
361        Bundle args = new Bundle();
362        args.putString(MEMBER_LOOKUP_URI_KEY, contactId);
363        args.putInt(MEMBER_ACTION_KEY, ADD_MEMBER);
364        getLoaderManager().restartLoader(LOADER_NEW_GROUP_MEMBER, args, mContactLoaderListener);
365    }
366
367    private void loadMemberToRemoveFromGroup(String lookupUri) {
368        Bundle args = new Bundle();
369        args.putString(MEMBER_LOOKUP_URI_KEY, lookupUri);
370        args.putInt(MEMBER_ACTION_KEY, REMOVE_MEMBER);
371        getLoaderManager().restartLoader(FULL_LOADER_NEW_GROUP_MEMBER, args,
372                mDataLoaderListener);
373    }
374
375    public void finishAddMember(Uri lookupUri) {
376        Toast.makeText(mContext, mContext.getString(R.string.groupMembershipChangeSavedToast),
377                Toast.LENGTH_SHORT).show();
378        getLoaderManager().destroyLoader(FULL_LOADER_NEW_GROUP_MEMBER);
379    }
380
381    public void finishRemoveMember(Uri lookupUri) {
382        Toast.makeText(mContext, mContext.getString(R.string.groupMembershipChangeSavedToast),
383                Toast.LENGTH_SHORT).show();
384        getLoaderManager().destroyLoader(FULL_LOADER_NEW_GROUP_MEMBER);
385        mMemberListAdapter.removeMember(mMemberToRemove);
386    }
387
388    public void setListener(Listener value) {
389        mListener = value;
390    }
391
392    @Override
393    public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
394        inflater.inflate(R.menu.edit_group, menu);
395    }
396
397    @Override
398    public boolean onOptionsItemSelected(MenuItem item) {
399        switch (item.getItemId()) {
400            case R.id.menu_done:
401                return save(SaveMode.CLOSE);
402            case R.id.menu_discard:
403                return revert();
404        }
405        return false;
406    }
407
408    private boolean revert() {
409        if (mGroupNameView.getText() != null &&
410                mGroupNameView.getText().toString().equals(mOriginalGroupName)) {
411            doRevertAction();
412        } else {
413            CancelEditDialogFragment.show(this);
414        }
415        return true;
416    }
417
418    private void doRevertAction() {
419        // When this Fragment is closed we don't want it to auto-save
420        mStatus = Status.CLOSING;
421        if (mListener != null) mListener.onReverted();
422    }
423
424    public static class CancelEditDialogFragment extends DialogFragment {
425
426        public static void show(GroupEditorFragment fragment) {
427            CancelEditDialogFragment dialog = new CancelEditDialogFragment();
428            dialog.setTargetFragment(fragment, 0);
429            dialog.show(fragment.getFragmentManager(), "cancelEditor");
430        }
431
432        @Override
433        public Dialog onCreateDialog(Bundle savedInstanceState) {
434            AlertDialog dialog = new AlertDialog.Builder(getActivity())
435                    .setIconAttribute(android.R.attr.alertDialogIcon)
436                    .setTitle(R.string.cancel_confirmation_dialog_title)
437                    .setMessage(R.string.cancel_confirmation_dialog_message)
438                    .setPositiveButton(R.string.discard,
439                        new DialogInterface.OnClickListener() {
440                            @Override
441                            public void onClick(DialogInterface dialog, int whichButton) {
442                                ((GroupEditorFragment) getTargetFragment()).doRevertAction();
443                            }
444                        }
445                    )
446                    .setNegativeButton(android.R.string.cancel, null)
447                    .create();
448            return dialog;
449        }
450    }
451
452    /**
453     * Saves or creates the group based on the mode, and if successful
454     * finishes the activity. This actually only handles saving the group name.
455     * @return true when successful
456     */
457    public boolean save(int saveMode) {
458        if (!hasValidGroupName() || mStatus != Status.EDITING) {
459            return false;
460        }
461
462        // If we are about to close the editor - there is no need to refresh the data
463        if (saveMode == SaveMode.CLOSE) {
464            getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS);
465        }
466
467        mStatus = Status.SAVING;
468
469        if (!hasChanges()) {
470            onSaveCompleted(false, saveMode, mGroupUri);
471            return true;
472        }
473
474        Activity activity = getActivity();
475        // If the activity is not there anymore, then we can't continue with the save process.
476        if (activity == null) {
477            return false;
478        }
479        Intent saveIntent = null;
480        if (mAction == Intent.ACTION_INSERT) {
481            saveIntent = ContactSaveService.createNewGroupIntent(activity,
482                    new Account(mAccountName, mAccountType), mGroupNameView.getText().toString(),
483                    activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED);
484        } else if (mAction == Intent.ACTION_EDIT) {
485            saveIntent = ContactSaveService.createGroupRenameIntent(activity, mGroupId,
486                    mGroupNameView.getText().toString(), activity.getClass(),
487                    GroupEditorActivity.ACTION_SAVE_COMPLETED);
488        } else {
489            throw new IllegalStateException("Invalid intent action type " + mAction);
490        }
491        activity.startService(saveIntent);
492        return true;
493    }
494
495    public void onSaveCompleted(boolean hadChanges, int saveMode, Uri groupUri) {
496        boolean success = groupUri != null;
497        Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + groupUri + ")");
498        if (hadChanges) {
499            Toast.makeText(mContext, success ? R.string.groupSavedToast :
500                    R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show();
501        }
502        switch (saveMode) {
503            case SaveMode.CLOSE:
504            case SaveMode.HOME:
505                final Intent resultIntent;
506                final int resultCode;
507                if (success && groupUri != null) {
508                    final String requestAuthority =
509                            groupUri == null ? null : groupUri.getAuthority();
510
511                    resultIntent = new Intent();
512                    if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) {
513                        // Build legacy Uri when requested by caller
514                        final long groupId = ContentUris.parseId(groupUri);
515                        final Uri legacyContentUri = Uri.parse("content://contacts/groups");
516                        final Uri legacyUri = ContentUris.withAppendedId(
517                                legacyContentUri, groupId);
518                        resultIntent.setData(legacyUri);
519                    } else {
520                        // Otherwise pass back the given Uri
521                        resultIntent.setData(groupUri);
522                    }
523
524                    resultCode = Activity.RESULT_OK;
525                } else {
526                    resultCode = Activity.RESULT_CANCELED;
527                    resultIntent = null;
528                }
529                // It is already saved, so prevent that it is saved again
530                mStatus = Status.CLOSING;
531                if (mListener != null) {
532                    mListener.onSaveFinished(resultCode, resultIntent, saveMode == SaveMode.HOME);
533                }
534                break;
535            case SaveMode.RELOAD:
536                // TODO: Handle reloading the group list
537            default:
538                throw new IllegalStateException("Unsupported save mode " + saveMode);
539        }
540    }
541
542    private boolean hasValidGroupName() {
543        return !TextUtils.isEmpty(mGroupNameView.getText());
544    }
545
546    private boolean hasChanges() {
547        return mGroupNameView.getText() != null &&
548                !mGroupNameView.getText().toString().equals(mOriginalGroupName);
549    }
550
551    /**
552     * The listener for the group metadata (i.e. group name, account type, and account name) loader.
553     */
554    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener =
555            new LoaderCallbacks<Cursor>() {
556
557        @Override
558        public CursorLoader onCreateLoader(int id, Bundle args) {
559            return new GroupMetaDataLoader(mContext, mGroupUri);
560        }
561
562        @Override
563        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
564            mStatus = Status.EDITING;
565            bindGroupMetaData(data);
566        }
567
568        @Override
569        public void onLoaderReset(Loader<Cursor> loader) {}
570    };
571
572    /**
573     * The loader listener for the list of existing group members.
574     */
575    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener =
576            new LoaderCallbacks<Cursor>() {
577
578        @Override
579        public CursorLoader onCreateLoader(int id, Bundle args) {
580            return new GroupMemberLoader(mContext, mGroupId);
581        }
582
583        @Override
584        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
585            List<Member> listMembers = new ArrayList<Member>();
586            List<Long> listContactIds = new ArrayList<Long>();
587            try {
588                data.moveToPosition(-1);
589                while (data.moveToNext()) {
590                    long contactId = data.getLong(GroupMemberLoader.CONTACT_ID_COLUMN_INDEX);
591                    String lookupKey = data.getString(
592                            GroupMemberLoader.CONTACT_LOOKUP_KEY_COLUMN_INDEX);
593                    String displayName = data.getString(
594                            GroupMemberLoader.CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
595                    String photoUri = data.getString(
596                            GroupMemberLoader.CONTACT_PHOTO_URI_COLUMN_INDEX);
597                    listMembers.add(new Member(lookupKey, contactId, displayName, photoUri));
598                    listContactIds.add(contactId);
599                }
600            } finally {
601                data.close();
602            }
603            // Update the list of displayed existing members
604            mMemberListAdapter.updateExistingMembersList(listMembers);
605            // Update the autocomplete adapter
606            mAutoCompleteAdapter.updateExistingMembersList(listContactIds);
607        }
608
609        @Override
610        public void onLoaderReset(Loader<Cursor> loader) {}
611    };
612
613    /**
614     * The listener to load a summary of details for a contact.
615     */
616    private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener =
617            new LoaderCallbacks<Cursor>() {
618
619        private int mMemberAction;
620
621        @Override
622        public CursorLoader onCreateLoader(int id, Bundle args) {
623            String memberId = args.getString(MEMBER_LOOKUP_URI_KEY);
624            mMemberAction = args.getInt(MEMBER_ACTION_KEY);
625            return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId),
626                    PROJECTION_CONTACT, null, null, null);
627        }
628
629        @Override
630        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
631            // Retrieve the contact data fields that will be sufficient to update the adapter with
632            // a new entry for this contact
633            Member member = null;
634            try {
635                cursor.moveToFirst();
636                long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
637                String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
638                String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
639                String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX);
640                getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER);
641                member = new Member(lookupKey, contactId, displayName, photoUri);
642            } finally {
643                cursor.close();
644            }
645
646            if (member == null) {
647                return;
648            }
649
650            // Don't do anything if the adapter already contains this member
651            // TODO: Come up with a better way to check membership using a DB query
652            if (mMemberListAdapter.contains(member)) {
653                Toast.makeText(getActivity(), getActivity().getString(
654                        R.string.contactAlreadyInGroup), Toast.LENGTH_SHORT).show();
655                return;
656            }
657
658            // Otherwise continue adding the member to list of members
659            mMemberListAdapter.addMember(member);
660
661            // Then start loading the full contact so that the change can be saved
662            // TODO: Combine these two loader steps into one. Either we get rid of the first loader
663            // (retrieving summary details) and just use the full contact loader, or find a way
664            // to save changes without loading the full contact
665            Bundle args = new Bundle();
666            args.putString(MEMBER_LOOKUP_URI_KEY, member.getLookupUri().toString());
667            args.putInt(MEMBER_ACTION_KEY, mMemberAction);
668            getLoaderManager().restartLoader(FULL_LOADER_NEW_GROUP_MEMBER, args,
669                    mDataLoaderListener);
670        }
671
672        @Override
673        public void onLoaderReset(Loader<Cursor> loader) {}
674    };
675
676    /**
677     * The listener for the loader that loads the full details of a contact so that when the data
678     * has arrived, the contact can be added or removed from the group.
679     */
680    private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDataLoaderListener =
681            new LoaderCallbacks<ContactLoader.Result>() {
682
683        private int mMemberAction;
684
685        @Override
686        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
687            mMemberAction = args.getInt(MEMBER_ACTION_KEY);
688            String memberLookupUri = args.getString(MEMBER_LOOKUP_URI_KEY);
689            return new ContactLoader(mContext, Uri.parse(memberLookupUri));
690        }
691
692        @Override
693        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
694            if (data == ContactLoader.Result.NOT_FOUND || data == ContactLoader.Result.ERROR) {
695                Log.i(TAG, "Contact was not found");
696                return;
697            }
698            saveChange(data, mMemberAction);
699        }
700
701        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
702        }
703    };
704
705    private void saveChange(ContactLoader.Result data, int action) {
706        EntityDeltaList state = EntityDeltaList.fromIterator(data.getEntities().iterator());
707
708        // We need a raw contact to save this group membership change to, so find the first valid
709        // {@link EntityDelta}.
710        // TODO: Find a better way to do this. This will not work if the group is associated with
711        // the other
712        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
713        AccountType type = null;
714        EntityDelta entity = null;
715        int size = state.size();
716        for (int i = 0; i < size; i++) {
717            entity = state.get(i);
718            final ValuesDelta values = entity.getValues();
719            if (!values.isVisible()) continue;
720
721            final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
722            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
723            type = accountTypes.getAccountType(accountType);
724            // If the account name and type match this group's properties and the account type is
725            // not an external type, then use this raw contact
726            if (mAccountName.equals(accountName) && mAccountType.equals(accountType) &&
727                    !type.isExternal()) {
728                break;
729            }
730        }
731
732        Intent intent = null;
733        switch (action) {
734            case ADD_MEMBER:
735                DataKind groupMembershipKind = type.getKindForMimetype(
736                        GroupMembership.CONTENT_ITEM_TYPE);
737                ValuesDelta entry = EntityModifier.insertChild(entity, groupMembershipKind);
738                entry.put(GroupMembership.GROUP_ROW_ID, mGroupId);
739                // Form intent
740                intent = ContactSaveService.createSaveContactIntent(getActivity(), state,
741                        SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE, getActivity().getClass(),
742                        GroupEditorActivity.ACTION_ADD_MEMBER_COMPLETED);
743                break;
744            case REMOVE_MEMBER:
745                // TODO: Check that the contact was in the group in the first place
746                ArrayList<ValuesDelta> entries = entity.getMimeEntries(
747                        GroupMembership.CONTENT_ITEM_TYPE);
748                if (entries != null) {
749                    for (ValuesDelta valuesDeltaEntry : entries) {
750                        if (!valuesDeltaEntry.isDelete()) {
751                            Long groupId = valuesDeltaEntry.getAsLong(GroupMembership.GROUP_ROW_ID);
752                            if (groupId == mGroupId) {
753                                valuesDeltaEntry.markDeleted();
754                            }
755                        }
756                    }
757                }
758                intent = ContactSaveService.createSaveContactIntent(getActivity(), state,
759                        SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE, getActivity().getClass(),
760                        GroupEditorActivity.ACTION_REMOVE_MEMBER_COMPLETED);
761                break;
762            default:
763                throw new IllegalStateException("Invalid action for a group member " + action);
764        }
765        getActivity().startService(intent);
766    }
767
768    /**
769     * This represents a single member of the current group.
770     */
771    public static class Member {
772        private final Uri mLookupUri;
773        private final String mDisplayName;
774        private final Uri mPhotoUri;
775
776        public Member(String lookupKey, long contactId, String displayName, String photoUri) {
777            mLookupUri = Contacts.getLookupUri(contactId, lookupKey);
778            mDisplayName = displayName;
779            mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null;
780        }
781
782        public Uri getLookupUri() {
783            return mLookupUri;
784        }
785
786        public String getDisplayName() {
787            return mDisplayName;
788        }
789
790        public Uri getPhotoUri() {
791            return mPhotoUri;
792        }
793
794        @Override
795        public boolean equals(Object object) {
796            if (object instanceof Member) {
797                Member otherMember = (Member) object;
798                return otherMember != null && Objects.equal(mLookupUri, otherMember.getLookupUri());
799            }
800            return false;
801        }
802    }
803
804    /**
805     * This adapter displays a list of members for the current group being edited.
806     */
807    private final class MemberListAdapter extends BaseAdapter {
808
809        private List<Member> mNewMembersList = new ArrayList<Member>();
810        private List<Member> mTotalList = new ArrayList<Member>();
811
812        public boolean contains(Member member) {
813            return mTotalList.contains(member);
814        }
815
816        public void addMember(Member member) {
817            mNewMembersList.add(member);
818            mTotalList.add(member);
819            notifyDataSetChanged();
820        }
821
822        public void removeMember(Member member) {
823            if (mNewMembersList.contains(member)) {
824                mNewMembersList.remove(member);
825            }
826            mTotalList.remove(member);
827            notifyDataSetChanged();
828        }
829
830        public void updateExistingMembersList(List<Member> existingMembers) {
831            mTotalList.clear();
832            mTotalList.addAll(mNewMembersList);
833            mTotalList.addAll(existingMembers);
834            notifyDataSetChanged();
835        }
836
837        @Override
838        public View getView(int position, View convertView, ViewGroup parent) {
839            View result;
840            if (convertView == null) {
841                result = mLayoutInflater.inflate(R.layout.group_member_item, parent, false);
842            } else {
843                result = convertView;
844            }
845            final Member member = getItem(position);
846
847            QuickContactBadge badge = (QuickContactBadge) result.findViewById(R.id.badge);
848            badge.assignContactUri(member.getLookupUri());
849
850            TextView name = (TextView) result.findViewById(R.id.name);
851            name.setText(member.getDisplayName());
852
853            View deleteButton = result.findViewById(R.id.delete_button_container);
854            deleteButton.setOnClickListener(new OnClickListener() {
855                @Override
856                public void onClick(View v) {
857                    loadMemberToRemoveFromGroup(member.getLookupUri().toString());
858                    // TODO: This is a hack to save the reference to the member that should be
859                    // removed. This won't work if the user tries to remove multiple times in a row
860                    // and reference is outdated. We actually need a hash map of member URIs to the
861                    // actual Member object. Before dealing with hash map though, hopefully we can
862                    // figure out how to batch save membership changes, which would eliminate the
863                    // need for this variable.
864                    mMemberToRemove = member;
865                }
866            });
867
868            mPhotoManager.loadPhoto(badge, member.getPhotoUri());
869            return result;
870        }
871
872        @Override
873        public int getCount() {
874            return mTotalList.size();
875        }
876
877        @Override
878        public Member getItem(int position) {
879            return mTotalList.get(position);
880        }
881
882        @Override
883        public int getItemViewType(int position) {
884            return 0;
885        }
886
887        @Override
888        public int getViewTypeCount() {
889            return 1;
890        }
891
892        @Override
893        public long getItemId(int position) {
894            return -1;
895        }
896
897        @Override
898        public boolean areAllItemsEnabled() {
899            return false;
900        }
901
902        @Override
903        public boolean isEnabled(int position) {
904            return false;
905        }
906    }
907}
908