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 android.accounts.Account;
20import android.app.Activity;
21import android.app.AlertDialog;
22import android.app.Dialog;
23import android.app.DialogFragment;
24import android.app.Fragment;
25import android.app.LoaderManager;
26import android.app.LoaderManager.LoaderCallbacks;
27import android.content.ContentResolver;
28import android.content.ContentUris;
29import android.content.Context;
30import android.content.CursorLoader;
31import android.content.DialogInterface;
32import android.content.Intent;
33import android.content.Loader;
34import android.database.Cursor;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.Parcel;
38import android.os.Parcelable;
39import android.provider.ContactsContract.Contacts;
40import android.provider.ContactsContract.Intents;
41import android.text.TextUtils;
42import android.util.Log;
43import android.view.LayoutInflater;
44import android.view.Menu;
45import android.view.MenuInflater;
46import android.view.MenuItem;
47import android.view.View;
48import android.view.View.OnClickListener;
49import android.view.ViewGroup;
50import android.widget.AdapterView;
51import android.widget.AdapterView.OnItemClickListener;
52import android.widget.AutoCompleteTextView;
53import android.widget.BaseAdapter;
54import android.widget.ImageView;
55import android.widget.ListView;
56import android.widget.QuickContactBadge;
57import android.widget.TextView;
58import android.widget.Toast;
59
60import com.android.contacts.ContactSaveService;
61import com.android.contacts.GroupMemberLoader;
62import com.android.contacts.GroupMemberLoader.GroupEditorQuery;
63import com.android.contacts.GroupMetaDataLoader;
64import com.android.contacts.R;
65import com.android.contacts.activities.GroupEditorActivity;
66import com.android.contacts.common.ContactPhotoManager;
67import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
68import com.android.contacts.common.model.account.AccountType;
69import com.android.contacts.common.model.account.AccountWithDataSet;
70import com.android.contacts.common.editor.SelectAccountDialogFragment;
71import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember;
72import com.android.contacts.common.model.AccountTypeManager;
73import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
74import com.android.contacts.common.util.ViewUtil;
75
76import com.google.common.base.Objects;
77
78import java.util.ArrayList;
79import java.util.List;
80
81public class GroupEditorFragment extends Fragment implements SelectAccountDialogFragment.Listener {
82    private static final String TAG = "GroupEditorFragment";
83
84    private static final String LEGACY_CONTACTS_AUTHORITY = "contacts";
85
86    private static final String KEY_ACTION = "action";
87    private static final String KEY_GROUP_URI = "groupUri";
88    private static final String KEY_GROUP_ID = "groupId";
89    private static final String KEY_STATUS = "status";
90    private static final String KEY_ACCOUNT_NAME = "accountName";
91    private static final String KEY_ACCOUNT_TYPE = "accountType";
92    private static final String KEY_DATA_SET = "dataSet";
93    private static final String KEY_GROUP_NAME_IS_READ_ONLY = "groupNameIsReadOnly";
94    private static final String KEY_ORIGINAL_GROUP_NAME = "originalGroupName";
95    private static final String KEY_MEMBERS_TO_ADD = "membersToAdd";
96    private static final String KEY_MEMBERS_TO_REMOVE = "membersToRemove";
97    private static final String KEY_MEMBERS_TO_DISPLAY = "membersToDisplay";
98
99    private static final String CURRENT_EDITOR_TAG = "currentEditorForAccount";
100
101    public static interface Listener {
102        /**
103         * Group metadata was not found, close the fragment now.
104         */
105        public void onGroupNotFound();
106
107        /**
108         * User has tapped Revert, close the fragment now.
109         */
110        void onReverted();
111
112        /**
113         * Contact was saved and the Fragment can now be closed safely.
114         */
115        void onSaveFinished(int resultCode, Intent resultIntent);
116
117        /**
118         * Fragment is created but there's no accounts set up.
119         */
120        void onAccountsNotFound();
121    }
122
123    private static final int LOADER_GROUP_METADATA = 1;
124    private static final int LOADER_EXISTING_MEMBERS = 2;
125    private static final int LOADER_NEW_GROUP_MEMBER = 3;
126
127    private static final String MEMBER_RAW_CONTACT_ID_KEY = "rawContactId";
128    private static final String MEMBER_LOOKUP_URI_KEY = "memberLookupUri";
129
130    protected static final String[] PROJECTION_CONTACT = new String[] {
131        Contacts._ID,                           // 0
132        Contacts.DISPLAY_NAME_PRIMARY,          // 1
133        Contacts.DISPLAY_NAME_ALTERNATIVE,      // 2
134        Contacts.SORT_KEY_PRIMARY,              // 3
135        Contacts.STARRED,                       // 4
136        Contacts.CONTACT_PRESENCE,              // 5
137        Contacts.CONTACT_CHAT_CAPABILITY,       // 6
138        Contacts.PHOTO_ID,                      // 7
139        Contacts.PHOTO_THUMBNAIL_URI,           // 8
140        Contacts.LOOKUP_KEY,                    // 9
141        Contacts.PHONETIC_NAME,                 // 10
142        Contacts.HAS_PHONE_NUMBER,              // 11
143        Contacts.IS_USER_PROFILE,               // 12
144    };
145
146    protected static final int CONTACT_ID_COLUMN_INDEX = 0;
147    protected static final int CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
148    protected static final int CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
149    protected static final int CONTACT_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
150    protected static final int CONTACT_STARRED_COLUMN_INDEX = 4;
151    protected static final int CONTACT_PRESENCE_STATUS_COLUMN_INDEX = 5;
152    protected static final int CONTACT_CHAT_CAPABILITY_COLUMN_INDEX = 6;
153    protected static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 7;
154    protected static final int CONTACT_PHOTO_URI_COLUMN_INDEX = 8;
155    protected static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 9;
156    protected static final int CONTACT_PHONETIC_NAME_COLUMN_INDEX = 10;
157    protected static final int CONTACT_HAS_PHONE_COLUMN_INDEX = 11;
158    protected static final int CONTACT_IS_USER_PROFILE = 12;
159
160    /**
161     * Modes that specify the status of the editor
162     */
163    public enum Status {
164        SELECTING_ACCOUNT, // Account select dialog is showing
165        LOADING,    // Loader is fetching the group metadata
166        EDITING,    // Not currently busy. We are waiting forthe user to enter data.
167        SAVING,     // Data is currently being saved
168        CLOSING     // Prevents any more saves
169    }
170
171    private Context mContext;
172    private String mAction;
173    private Bundle mIntentExtras;
174    private Uri mGroupUri;
175    private long mGroupId;
176    private Listener mListener;
177
178    private Status mStatus;
179
180    private ViewGroup mRootView;
181    private ListView mListView;
182    private LayoutInflater mLayoutInflater;
183
184    private TextView mGroupNameView;
185    private AutoCompleteTextView mAutoCompleteTextView;
186
187    private String mAccountName;
188    private String mAccountType;
189    private String mDataSet;
190
191    private boolean mGroupNameIsReadOnly;
192    private String mOriginalGroupName = "";
193    private int mLastGroupEditorId;
194
195    private MemberListAdapter mMemberListAdapter;
196    private ContactPhotoManager mPhotoManager;
197
198    private ContentResolver mContentResolver;
199    private SuggestedMemberListAdapter mAutoCompleteAdapter;
200
201    private ArrayList<Member> mListMembersToAdd = new ArrayList<Member>();
202    private ArrayList<Member> mListMembersToRemove = new ArrayList<Member>();
203    private ArrayList<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.EXTRA_ACCOUNT);
243            final String dataSet = mIntentExtras == null ? null :
244                    mIntentExtras.getString(Intents.Insert.EXTRA_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
262    private void startGroupMetaDataLoader() {
263        mStatus = Status.LOADING;
264        getLoaderManager().initLoader(LOADER_GROUP_METADATA, null,
265                mGroupMetaDataLoaderListener);
266    }
267
268    @Override
269    public void onSaveInstanceState(Bundle outState) {
270        super.onSaveInstanceState(outState);
271        outState.putString(KEY_ACTION, mAction);
272        outState.putParcelable(KEY_GROUP_URI, mGroupUri);
273        outState.putLong(KEY_GROUP_ID, mGroupId);
274
275        outState.putSerializable(KEY_STATUS, mStatus);
276        outState.putString(KEY_ACCOUNT_NAME, mAccountName);
277        outState.putString(KEY_ACCOUNT_TYPE, mAccountType);
278        outState.putString(KEY_DATA_SET, mDataSet);
279
280        outState.putBoolean(KEY_GROUP_NAME_IS_READ_ONLY, mGroupNameIsReadOnly);
281        outState.putString(KEY_ORIGINAL_GROUP_NAME, mOriginalGroupName);
282
283        outState.putParcelableArrayList(KEY_MEMBERS_TO_ADD, mListMembersToAdd);
284        outState.putParcelableArrayList(KEY_MEMBERS_TO_REMOVE, mListMembersToRemove);
285        outState.putParcelableArrayList(KEY_MEMBERS_TO_DISPLAY, mListToDisplay);
286    }
287
288    private void onRestoreInstanceState(Bundle state) {
289        mAction = state.getString(KEY_ACTION);
290        mGroupUri = state.getParcelable(KEY_GROUP_URI);
291        mGroupId = state.getLong(KEY_GROUP_ID);
292
293        mStatus = (Status) state.getSerializable(KEY_STATUS);
294        mAccountName = state.getString(KEY_ACCOUNT_NAME);
295        mAccountType = state.getString(KEY_ACCOUNT_TYPE);
296        mDataSet = state.getString(KEY_DATA_SET);
297
298        mGroupNameIsReadOnly = state.getBoolean(KEY_GROUP_NAME_IS_READ_ONLY);
299        mOriginalGroupName = state.getString(KEY_ORIGINAL_GROUP_NAME);
300
301        mListMembersToAdd = state.getParcelableArrayList(KEY_MEMBERS_TO_ADD);
302        mListMembersToRemove = state.getParcelableArrayList(KEY_MEMBERS_TO_REMOVE);
303        mListToDisplay = state.getParcelableArrayList(KEY_MEMBERS_TO_DISPLAY);
304    }
305
306    public void setContentResolver(ContentResolver resolver) {
307        mContentResolver = resolver;
308        if (mAutoCompleteAdapter != null) {
309            mAutoCompleteAdapter.setContentResolver(mContentResolver);
310        }
311    }
312
313    private void selectAccountAndCreateGroup() {
314        final List<AccountWithDataSet> accounts =
315                AccountTypeManager.getInstance(mContext).getAccounts(true /* writeable */);
316        // No Accounts available
317        if (accounts.isEmpty()) {
318            Log.e(TAG, "No accounts were found.");
319            if (mListener != null) {
320                mListener.onAccountsNotFound();
321            }
322            return;
323        }
324
325        // In the common case of a single account being writable, auto-select
326        // it without showing a dialog.
327        if (accounts.size() == 1) {
328            mAccountName = accounts.get(0).name;
329            mAccountType = accounts.get(0).type;
330            mDataSet = accounts.get(0).dataSet;
331            setupEditorForAccount();
332            return;  // Don't show a dialog.
333        }
334
335        mStatus = Status.SELECTING_ACCOUNT;
336        SelectAccountDialogFragment.show(getFragmentManager(), this,
337                R.string.dialog_new_group_account, AccountListFilter.ACCOUNTS_GROUP_WRITABLE,
338                null);
339    }
340
341    @Override
342    public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
343        mAccountName = account.name;
344        mAccountType = account.type;
345        mDataSet = account.dataSet;
346        setupEditorForAccount();
347    }
348
349    @Override
350    public void onAccountSelectorCancelled() {
351        if (mListener != null) {
352            // Exit the fragment because we cannot continue without selecting an account
353            mListener.onGroupNotFound();
354        }
355    }
356
357    private AccountType getAccountType() {
358        return AccountTypeManager.getInstance(mContext).getAccountType(mAccountType, mDataSet);
359    }
360
361    /**
362     * @return true if the group membership is editable on this account type.  false otherwise,
363     *         or account is not set yet.
364     */
365    private boolean isGroupMembershipEditable() {
366        if (mAccountType == null) {
367            return false;
368        }
369        return getAccountType().isGroupMembershipEditable();
370    }
371
372    /**
373     * Sets up the editor based on the group's account name and type.
374     */
375    private void setupEditorForAccount() {
376        final AccountType accountType = getAccountType();
377        final boolean editable = isGroupMembershipEditable();
378        boolean isNewEditor = false;
379        mMemberListAdapter.setIsGroupMembershipEditable(editable);
380
381        // Since this method can be called multiple time, remove old editor if the editor type
382        // is different from the new one and mark the editor with a tag so it can be found for
383        // removal if needed
384        View editorView;
385        int newGroupEditorId =
386                editable ? R.layout.group_editor_view : R.layout.external_group_editor_view;
387        if (newGroupEditorId != mLastGroupEditorId) {
388            View oldEditorView = mRootView.findViewWithTag(CURRENT_EDITOR_TAG);
389            if (oldEditorView != null) {
390                mRootView.removeView(oldEditorView);
391            }
392            editorView = mLayoutInflater.inflate(newGroupEditorId, mRootView, false);
393            editorView.setTag(CURRENT_EDITOR_TAG);
394            mAutoCompleteAdapter = null;
395            mLastGroupEditorId = newGroupEditorId;
396            isNewEditor = true;
397        } else {
398            editorView = mRootView.findViewWithTag(CURRENT_EDITOR_TAG);
399            if (editorView == null) {
400                throw new IllegalStateException("Group editor view not found");
401            }
402        }
403
404        mGroupNameView = (TextView) editorView.findViewById(R.id.group_name);
405        mAutoCompleteTextView = (AutoCompleteTextView) editorView.findViewById(
406                R.id.add_member_field);
407
408        mListView = (ListView) editorView.findViewById(android.R.id.list);
409        mListView.setAdapter(mMemberListAdapter);
410
411        // Setup the account header, only when exists.
412        if (editorView.findViewById(R.id.account_header) != null) {
413            CharSequence accountTypeDisplayLabel = accountType.getDisplayLabel(mContext);
414            ImageView accountIcon = (ImageView) editorView.findViewById(R.id.account_icon);
415            TextView accountTypeTextView = (TextView) editorView.findViewById(R.id.account_type);
416            TextView accountNameTextView = (TextView) editorView.findViewById(R.id.account_name);
417            if (!TextUtils.isEmpty(mAccountName)) {
418                accountNameTextView.setText(
419                        mContext.getString(R.string.from_account_format, mAccountName));
420            }
421            accountTypeTextView.setText(accountTypeDisplayLabel);
422            accountIcon.setImageDrawable(accountType.getDisplayIcon(mContext));
423        }
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();
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                    .setMessage(R.string.cancel_confirmation_dialog_message)
554                    .setPositiveButton(android.R.string.ok,
555                        new DialogInterface.OnClickListener() {
556                            @Override
557                            public void onClick(DialogInterface dialogInterface, int whichButton) {
558                                ((GroupEditorFragment) getTargetFragment()).doRevertAction();
559                            }
560                        }
561                    )
562                    .setNegativeButton(android.R.string.cancel, null)
563                    .create();
564            return dialog;
565        }
566    }
567
568    /**
569     * Saves or creates the group based on the mode, and if successful
570     * finishes the activity. This actually only handles saving the group name.
571     * @return true when successful
572     */
573    public boolean save() {
574        if (!hasValidGroupName() || mStatus != Status.EDITING) {
575            mStatus = Status.CLOSING;
576            if (mListener != null) {
577                mListener.onReverted();
578            }
579            return false;
580        }
581
582        // If we are about to close the editor - there is no need to refresh the data
583        getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS);
584
585        // If there are no changes, then go straight to onSaveCompleted()
586        if (!hasNameChange() && !hasMembershipChange()) {
587            onSaveCompleted(false, mGroupUri);
588            return true;
589        }
590
591        mStatus = Status.SAVING;
592
593        Activity activity = getActivity();
594        // If the activity is not there anymore, then we can't continue with the save process.
595        if (activity == null) {
596            return false;
597        }
598        Intent saveIntent = null;
599        if (Intent.ACTION_INSERT.equals(mAction)) {
600            // Create array of raw contact IDs for contacts to add to the group
601            long[] membersToAddArray = convertToArray(mListMembersToAdd);
602
603            // Create the save intent to create the group and add members at the same time
604            saveIntent = ContactSaveService.createNewGroupIntent(activity,
605                    new AccountWithDataSet(mAccountName, mAccountType, mDataSet),
606                    mGroupNameView.getText().toString(),
607                    membersToAddArray, activity.getClass(),
608                    GroupEditorActivity.ACTION_SAVE_COMPLETED);
609        } else if (Intent.ACTION_EDIT.equals(mAction)) {
610            // Create array of raw contact IDs for contacts to add to the group
611            long[] membersToAddArray = convertToArray(mListMembersToAdd);
612
613            // Create array of raw contact IDs for contacts to add to the group
614            long[] membersToRemoveArray = convertToArray(mListMembersToRemove);
615
616            // Create the update intent (which includes the updated group name if necessary)
617            saveIntent = ContactSaveService.createGroupUpdateIntent(activity, mGroupId,
618                    getUpdatedName(), membersToAddArray, membersToRemoveArray,
619                    activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED);
620        } else {
621            throw new IllegalStateException("Invalid intent action type " + mAction);
622        }
623        activity.startService(saveIntent);
624        return true;
625    }
626
627    public void onSaveCompleted(boolean hadChanges, Uri groupUri) {
628        boolean success = groupUri != null;
629        Log.d(TAG, "onSaveCompleted(" + groupUri + ")");
630        if (hadChanges) {
631            Toast.makeText(mContext, success ? R.string.groupSavedToast :
632                    R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show();
633        }
634        final Intent resultIntent;
635        final int resultCode;
636        if (success && groupUri != null) {
637            final String requestAuthority = groupUri.getAuthority();
638
639            resultIntent = new Intent();
640            if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) {
641                // Build legacy Uri when requested by caller
642                final long groupId = ContentUris.parseId(groupUri);
643                final Uri legacyContentUri = Uri.parse("content://contacts/groups");
644                final Uri legacyUri = ContentUris.withAppendedId(
645                        legacyContentUri, groupId);
646                resultIntent.setData(legacyUri);
647            } else {
648                // Otherwise pass back the given Uri
649                resultIntent.setData(groupUri);
650            }
651
652            resultCode = Activity.RESULT_OK;
653        } else {
654            resultCode = Activity.RESULT_CANCELED;
655            resultIntent = null;
656        }
657        // It is already saved, so prevent that it is saved again
658        mStatus = Status.CLOSING;
659        if (mListener != null) {
660            mListener.onSaveFinished(resultCode, resultIntent);
661        }
662    }
663
664    private boolean hasValidGroupName() {
665        return mGroupNameView != null && !TextUtils.isEmpty(mGroupNameView.getText());
666    }
667
668    private boolean hasNameChange() {
669        return mGroupNameView != null &&
670                !mGroupNameView.getText().toString().equals(mOriginalGroupName);
671    }
672
673    private boolean hasMembershipChange() {
674        return mListMembersToAdd.size() > 0 || mListMembersToRemove.size() > 0;
675    }
676
677    /**
678     * Returns the group's new name or null if there is no change from the
679     * original name that was loaded for the group.
680     */
681    private String getUpdatedName() {
682        String groupNameFromTextView = mGroupNameView.getText().toString();
683        if (groupNameFromTextView.equals(mOriginalGroupName)) {
684            // No name change, so return null
685            return null;
686        }
687        return groupNameFromTextView;
688    }
689
690    private static long[] convertToArray(List<Member> listMembers) {
691        int size = listMembers.size();
692        long[] membersArray = new long[size];
693        for (int i = 0; i < size; i++) {
694            membersArray[i] = listMembers.get(i).getRawContactId();
695        }
696        return membersArray;
697    }
698
699    private void addExistingMembers(List<Member> members) {
700
701        // Re-create the list to display
702        mListToDisplay.clear();
703        mListToDisplay.addAll(members);
704        mListToDisplay.addAll(mListMembersToAdd);
705        mListToDisplay.removeAll(mListMembersToRemove);
706        mMemberListAdapter.notifyDataSetChanged();
707
708
709        // Update the autocomplete adapter (if there is one) so these contacts don't get suggested
710        if (mAutoCompleteAdapter != null) {
711            mAutoCompleteAdapter.updateExistingMembersList(members);
712        }
713    }
714
715    private void addMember(Member member) {
716        // Update the display list
717        mListMembersToAdd.add(member);
718        mListToDisplay.add(member);
719        mMemberListAdapter.notifyDataSetChanged();
720
721        // Update the autocomplete adapter so the contact doesn't get suggested again
722        mAutoCompleteAdapter.addNewMember(member.getContactId());
723    }
724
725    private void removeMember(Member member) {
726        // If the contact was just added during this session, remove it from the list of
727        // members to add
728        if (mListMembersToAdd.contains(member)) {
729            mListMembersToAdd.remove(member);
730        } else {
731            // Otherwise this contact was already part of the existing list of contacts,
732            // so we need to do a content provider deletion operation
733            mListMembersToRemove.add(member);
734        }
735        // In either case, update the UI so the contact is no longer in the list of
736        // members
737        mListToDisplay.remove(member);
738        mMemberListAdapter.notifyDataSetChanged();
739
740        // Update the autocomplete adapter so the contact can get suggested again
741        mAutoCompleteAdapter.removeMember(member.getContactId());
742    }
743
744    /**
745     * The listener for the group metadata (i.e. group name, account type, and account name) loader.
746     */
747    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener =
748            new LoaderCallbacks<Cursor>() {
749
750        @Override
751        public CursorLoader onCreateLoader(int id, Bundle args) {
752            return new GroupMetaDataLoader(mContext, mGroupUri);
753        }
754
755        @Override
756        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
757            bindGroupMetaData(data);
758
759            // Load existing members
760            getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null,
761                    mGroupMemberListLoaderListener);
762        }
763
764        @Override
765        public void onLoaderReset(Loader<Cursor> loader) {}
766    };
767
768    /**
769     * The loader listener for the list of existing group members.
770     */
771    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener =
772            new LoaderCallbacks<Cursor>() {
773
774        @Override
775        public CursorLoader onCreateLoader(int id, Bundle args) {
776            return GroupMemberLoader.constructLoaderForGroupEditorQuery(mContext, mGroupId);
777        }
778
779        @Override
780        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
781            List<Member> listExistingMembers = new ArrayList<Member>();
782            data.moveToPosition(-1);
783            while (data.moveToNext()) {
784                long contactId = data.getLong(GroupEditorQuery.CONTACT_ID);
785                long rawContactId = data.getLong(GroupEditorQuery.RAW_CONTACT_ID);
786                String lookupKey = data.getString(GroupEditorQuery.CONTACT_LOOKUP_KEY);
787                String displayName = data.getString(GroupEditorQuery.CONTACT_DISPLAY_NAME_PRIMARY);
788                String photoUri = data.getString(GroupEditorQuery.CONTACT_PHOTO_URI);
789                listExistingMembers.add(new Member(rawContactId, lookupKey, contactId,
790                        displayName, photoUri));
791            }
792
793            // Update the display list
794            addExistingMembers(listExistingMembers);
795
796            // No more updates
797            // TODO: move to a runnable
798            getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS);
799        }
800
801        @Override
802        public void onLoaderReset(Loader<Cursor> loader) {}
803    };
804
805    /**
806     * The listener to load a summary of details for a contact.
807     */
808    // TODO: Remove this step because showing the aggregate contact can be confusing when the user
809    // just selected a raw contact
810    private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener =
811            new LoaderCallbacks<Cursor>() {
812
813        private long mRawContactId;
814
815        @Override
816        public CursorLoader onCreateLoader(int id, Bundle args) {
817            String memberId = args.getString(MEMBER_LOOKUP_URI_KEY);
818            mRawContactId = args.getLong(MEMBER_RAW_CONTACT_ID_KEY);
819            return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId),
820                    PROJECTION_CONTACT, null, null, null);
821        }
822
823        @Override
824        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
825            if (!cursor.moveToFirst()) {
826                return;
827            }
828            // Retrieve the contact data fields that will be sufficient to update the adapter with
829            // a new entry for this contact
830            long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
831            String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
832            String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
833            String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX);
834            getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER);
835            Member member = new Member(mRawContactId, lookupKey, contactId, displayName, photoUri);
836            addMember(member);
837        }
838
839        @Override
840        public void onLoaderReset(Loader<Cursor> loader) {}
841    };
842
843    /**
844     * This represents a single member of the current group.
845     */
846    public static class Member implements Parcelable {
847
848        // TODO: Switch to just dealing with raw contact IDs everywhere if possible
849        private final long mRawContactId;
850        private final long mContactId;
851        private final Uri mLookupUri;
852        private final String mDisplayName;
853        private final Uri mPhotoUri;
854        private final String mLookupKey;
855
856        public Member(long rawContactId, String lookupKey, long contactId, String displayName,
857                String photoUri) {
858            mRawContactId = rawContactId;
859            mContactId = contactId;
860            mLookupKey = lookupKey;
861            mLookupUri = Contacts.getLookupUri(contactId, lookupKey);
862            mDisplayName = displayName;
863            mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null;
864        }
865
866        public long getRawContactId() {
867            return mRawContactId;
868        }
869
870        public long getContactId() {
871            return mContactId;
872        }
873
874        public Uri getLookupUri() {
875            return mLookupUri;
876        }
877
878        public String getLookupKey() {
879            return mLookupKey;
880        }
881
882        public String getDisplayName() {
883            return mDisplayName;
884        }
885
886        public Uri getPhotoUri() {
887            return mPhotoUri;
888        }
889
890        @Override
891        public boolean equals(Object object) {
892            if (object instanceof Member) {
893                Member otherMember = (Member) object;
894                return Objects.equal(mLookupUri, otherMember.getLookupUri());
895            }
896            return false;
897        }
898
899        @Override
900        public int hashCode() {
901            return mLookupUri == null ? 0 : mLookupUri.hashCode();
902        }
903
904        // Parcelable
905        @Override
906        public int describeContents() {
907            return 0;
908        }
909
910        @Override
911        public void writeToParcel(Parcel dest, int flags) {
912            dest.writeLong(mRawContactId);
913            dest.writeLong(mContactId);
914            dest.writeParcelable(mLookupUri, flags);
915            dest.writeString(mLookupKey);
916            dest.writeString(mDisplayName);
917            dest.writeParcelable(mPhotoUri, flags);
918        }
919
920        private Member(Parcel in) {
921            mRawContactId = in.readLong();
922            mContactId = in.readLong();
923            mLookupUri = in.readParcelable(getClass().getClassLoader());
924            mLookupKey = in.readString();
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            @Override
931            public Member createFromParcel(Parcel in) {
932                return new Member(in);
933            }
934
935            @Override
936            public Member[] newArray(int size) {
937                return new Member[size];
938            }
939        };
940    }
941
942    /**
943     * This adapter displays a list of members for the current group being edited.
944     */
945    private final class MemberListAdapter extends BaseAdapter {
946
947        private boolean mIsGroupMembershipEditable = true;
948
949        @Override
950        public View getView(int position, View convertView, ViewGroup parent) {
951            View result;
952            if (convertView == null) {
953                result = mLayoutInflater.inflate(mIsGroupMembershipEditable ?
954                        R.layout.group_member_item : R.layout.external_group_member_item,
955                        parent, false);
956            } else {
957                result = convertView;
958            }
959            final Member member = getItem(position);
960
961            QuickContactBadge badge = (QuickContactBadge) result.findViewById(R.id.badge);
962            badge.assignContactUri(member.getLookupUri());
963
964            TextView name = (TextView) result.findViewById(R.id.name);
965            name.setText(member.getDisplayName());
966
967            View deleteButton = result.findViewById(R.id.delete_button_container);
968            if (deleteButton != null) {
969                deleteButton.setOnClickListener(new OnClickListener() {
970                    @Override
971                    public void onClick(View v) {
972                        removeMember(member);
973                    }
974                });
975            }
976            DefaultImageRequest request = new DefaultImageRequest(member.getDisplayName(),
977                    member.getLookupKey(), true /* isCircular */);
978            mPhotoManager.loadPhoto(badge, member.getPhotoUri(),
979                    ViewUtil.getConstantPreLayoutWidth(badge), false, true /* isCircular */,
980                            request);
981            return result;
982        }
983
984        @Override
985        public int getCount() {
986            return mListToDisplay.size();
987        }
988
989        @Override
990        public Member getItem(int position) {
991            return mListToDisplay.get(position);
992        }
993
994        @Override
995        public long getItemId(int position) {
996            return position;
997        }
998
999        public void setIsGroupMembershipEditable(boolean editable) {
1000            mIsGroupMembershipEditable = editable;
1001        }
1002    }
1003}
1004