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