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