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