Home | History | Annotate | Download | only in group
      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 
     17 package com.android.contacts.group;
     18 
     19 import android.accounts.Account;
     20 import android.app.Activity;
     21 import android.app.AlertDialog;
     22 import android.app.Dialog;
     23 import android.app.DialogFragment;
     24 import android.app.Fragment;
     25 import android.app.LoaderManager;
     26 import android.app.LoaderManager.LoaderCallbacks;
     27 import android.content.ContentResolver;
     28 import android.content.ContentUris;
     29 import android.content.Context;
     30 import android.content.CursorLoader;
     31 import android.content.DialogInterface;
     32 import android.content.Intent;
     33 import android.content.Loader;
     34 import android.database.Cursor;
     35 import android.net.Uri;
     36 import android.os.Bundle;
     37 import android.os.Parcel;
     38 import android.os.Parcelable;
     39 import android.provider.ContactsContract.Contacts;
     40 import android.provider.ContactsContract.Intents;
     41 import android.text.TextUtils;
     42 import android.util.Log;
     43 import android.view.LayoutInflater;
     44 import android.view.Menu;
     45 import android.view.MenuInflater;
     46 import android.view.MenuItem;
     47 import android.view.View;
     48 import android.view.View.OnClickListener;
     49 import android.view.ViewGroup;
     50 import android.widget.AdapterView;
     51 import android.widget.AdapterView.OnItemClickListener;
     52 import android.widget.AutoCompleteTextView;
     53 import android.widget.BaseAdapter;
     54 import android.widget.ImageView;
     55 import android.widget.ListView;
     56 import android.widget.QuickContactBadge;
     57 import android.widget.TextView;
     58 import android.widget.Toast;
     59 
     60 import com.android.contacts.ContactSaveService;
     61 import com.android.contacts.GroupMemberLoader;
     62 import com.android.contacts.GroupMemberLoader.GroupEditorQuery;
     63 import com.android.contacts.GroupMetaDataLoader;
     64 import com.android.contacts.R;
     65 import com.android.contacts.activities.GroupEditorActivity;
     66 import com.android.contacts.common.ContactPhotoManager;
     67 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
     68 import com.android.contacts.common.model.account.AccountType;
     69 import com.android.contacts.common.model.account.AccountWithDataSet;
     70 import com.android.contacts.common.editor.SelectAccountDialogFragment;
     71 import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember;
     72 import com.android.contacts.common.model.AccountTypeManager;
     73 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
     74 import com.android.contacts.common.util.ViewUtil;
     75 
     76 import com.google.common.base.Objects;
     77 
     78 import java.util.ArrayList;
     79 import java.util.List;
     80 
     81 public 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.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 
    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