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          */
onGroupNotFound()105         public void onGroupNotFound();
106 
107         /**
108          * User has tapped Revert, close the fragment now.
109          */
onReverted()110         void onReverted();
111 
112         /**
113          * Contact was saved and the Fragment can now be closed safely.
114          */
onSaveFinished(int resultCode, Intent resultIntent)115         void onSaveFinished(int resultCode, Intent resultIntent);
116 
117         /**
118          * Fragment is created but there's no accounts set up.
119          */
onAccountsNotFound()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 
GroupEditorFragment()205     public GroupEditorFragment() {
206     }
207 
208     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)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
onAttach(Activity activity)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
onActivityCreated(Bundle savedInstanceState)225     public void onActivityCreated(Bundle savedInstanceState) {
226         super.onActivityCreated(savedInstanceState);
227 
228         if (savedInstanceState != null) {
229             // Just restore from the saved state.  No loading.
230             onRestoreInstanceState(savedInstanceState);
231             if (mStatus == Status.SELECTING_ACCOUNT) {
232                 // Account select dialog is showing.  Don't setup the editor yet.
233             } else if (mStatus == Status.LOADING) {
234                 startGroupMetaDataLoader();
235             } else {
236                 setupEditorForAccount();
237             }
238         } else if (Intent.ACTION_EDIT.equals(mAction)) {
239             startGroupMetaDataLoader();
240         } else if (Intent.ACTION_INSERT.equals(mAction)) {
241             final Account account = mIntentExtras == null ? null :
242                     (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
243             final String dataSet = mIntentExtras == null ? null :
244                     mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
245 
246             if (account != null) {
247                 // Account specified in Intent - no data set can be specified in this manner.
248                 mAccountName = account.name;
249                 mAccountType = account.type;
250                 mDataSet = dataSet;
251                 setupEditorForAccount();
252             } else {
253                 // No Account specified. Let the user choose from a disambiguation dialog.
254                 selectAccountAndCreateGroup();
255             }
256         } else {
257             throw new IllegalArgumentException("Unknown Action String " + mAction +
258                     ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
259         }
260     }
261 
startGroupMetaDataLoader()262     private void startGroupMetaDataLoader() {
263         mStatus = Status.LOADING;
264         getLoaderManager().initLoader(LOADER_GROUP_METADATA, null,
265                 mGroupMetaDataLoaderListener);
266     }
267 
268     @Override
onSaveInstanceState(Bundle outState)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 
onRestoreInstanceState(Bundle state)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 
setContentResolver(ContentResolver resolver)306     public void setContentResolver(ContentResolver resolver) {
307         mContentResolver = resolver;
308         if (mAutoCompleteAdapter != null) {
309             mAutoCompleteAdapter.setContentResolver(mContentResolver);
310         }
311     }
312 
selectAccountAndCreateGroup()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
onAccountChosen(AccountWithDataSet account, Bundle extraArgs)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
onAccountSelectorCancelled()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 
getAccountType()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      */
isGroupMembershipEditable()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      */
setupEditorForAccount()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 
load(String action, Uri groupUri, Bundle intentExtras)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 
bindGroupMetaData(Cursor cursor)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 
loadMemberToAddToGroup(long rawContactId, String contactId)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 
setListener(Listener value)499     public void setListener(Listener value) {
500         mListener = value;
501     }
502 
onDoneClicked()503     public void onDoneClicked() {
504         if (isGroupMembershipEditable()) {
505             save();
506         } else {
507             // Just revert it.
508             doRevertAction();
509         }
510     }
511 
512     @Override
onCreateOptionsMenu(Menu menu, final MenuInflater inflater)513     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
514         inflater.inflate(R.menu.edit_group, menu);
515     }
516 
doRevertAction()517     private void doRevertAction() {
518         // When this Fragment is closed we don't want it to auto-save
519         mStatus = Status.CLOSING;
520         if (mListener != null) mListener.onReverted();
521     }
522 
523     public static class CancelEditDialogFragment extends DialogFragment {
524 
show(GroupEditorFragment fragment)525         public static void show(GroupEditorFragment fragment) {
526             CancelEditDialogFragment dialog = new CancelEditDialogFragment();
527             dialog.setTargetFragment(fragment, 0);
528             dialog.show(fragment.getFragmentManager(), "cancelEditor");
529         }
530 
531         @Override
onCreateDialog(Bundle savedInstanceState)532         public Dialog onCreateDialog(Bundle savedInstanceState) {
533             AlertDialog dialog = new AlertDialog.Builder(getActivity())
534                     .setIconAttribute(android.R.attr.alertDialogIcon)
535                     .setMessage(R.string.cancel_confirmation_dialog_message)
536                     .setPositiveButton(android.R.string.ok,
537                         new DialogInterface.OnClickListener() {
538                             @Override
539                             public void onClick(DialogInterface dialogInterface, int whichButton) {
540                                 ((GroupEditorFragment) getTargetFragment()).doRevertAction();
541                             }
542                         }
543                     )
544                     .setNegativeButton(android.R.string.cancel, null)
545                     .create();
546             return dialog;
547         }
548     }
549 
550     /**
551      * Saves or creates the group based on the mode, and if successful
552      * finishes the activity. This actually only handles saving the group name.
553      * @return true when successful
554      */
save()555     public boolean save() {
556         if (!hasValidGroupName() || mStatus != Status.EDITING) {
557             mStatus = Status.CLOSING;
558             if (mListener != null) {
559                 mListener.onReverted();
560             }
561             return false;
562         }
563 
564         // If we are about to close the editor - there is no need to refresh the data
565         getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS);
566 
567         // If there are no changes, then go straight to onSaveCompleted()
568         if (!hasNameChange() && !hasMembershipChange()) {
569             onSaveCompleted(false, mGroupUri);
570             return true;
571         }
572 
573         mStatus = Status.SAVING;
574 
575         Activity activity = getActivity();
576         // If the activity is not there anymore, then we can't continue with the save process.
577         if (activity == null) {
578             return false;
579         }
580         Intent saveIntent = null;
581         if (Intent.ACTION_INSERT.equals(mAction)) {
582             // Create array of raw contact IDs for contacts to add to the group
583             long[] membersToAddArray = convertToArray(mListMembersToAdd);
584 
585             // Create the save intent to create the group and add members at the same time
586             saveIntent = ContactSaveService.createNewGroupIntent(activity,
587                     new AccountWithDataSet(mAccountName, mAccountType, mDataSet),
588                     mGroupNameView.getText().toString(),
589                     membersToAddArray, activity.getClass(),
590                     GroupEditorActivity.ACTION_SAVE_COMPLETED);
591         } else if (Intent.ACTION_EDIT.equals(mAction)) {
592             // Create array of raw contact IDs for contacts to add to the group
593             long[] membersToAddArray = convertToArray(mListMembersToAdd);
594 
595             // Create array of raw contact IDs for contacts to add to the group
596             long[] membersToRemoveArray = convertToArray(mListMembersToRemove);
597 
598             // Create the update intent (which includes the updated group name if necessary)
599             saveIntent = ContactSaveService.createGroupUpdateIntent(activity, mGroupId,
600                     getUpdatedName(), membersToAddArray, membersToRemoveArray,
601                     activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED);
602         } else {
603             throw new IllegalStateException("Invalid intent action type " + mAction);
604         }
605         activity.startService(saveIntent);
606         return true;
607     }
608 
onSaveCompleted(boolean hadChanges, Uri groupUri)609     public void onSaveCompleted(boolean hadChanges, Uri groupUri) {
610         boolean success = groupUri != null;
611         Log.d(TAG, "onSaveCompleted(" + groupUri + ")");
612         if (hadChanges) {
613             Toast.makeText(mContext, success ? R.string.groupSavedToast :
614                     R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show();
615         }
616         final Intent resultIntent;
617         final int resultCode;
618         if (success && groupUri != null) {
619             final String requestAuthority = groupUri.getAuthority();
620 
621             resultIntent = new Intent();
622             if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) {
623                 // Build legacy Uri when requested by caller
624                 final long groupId = ContentUris.parseId(groupUri);
625                 final Uri legacyContentUri = Uri.parse("content://contacts/groups");
626                 final Uri legacyUri = ContentUris.withAppendedId(
627                         legacyContentUri, groupId);
628                 resultIntent.setData(legacyUri);
629             } else {
630                 // Otherwise pass back the given Uri
631                 resultIntent.setData(groupUri);
632             }
633 
634             resultCode = Activity.RESULT_OK;
635         } else {
636             resultCode = Activity.RESULT_CANCELED;
637             resultIntent = null;
638         }
639         // It is already saved, so prevent that it is saved again
640         mStatus = Status.CLOSING;
641         if (mListener != null) {
642             mListener.onSaveFinished(resultCode, resultIntent);
643         }
644     }
645 
hasValidGroupName()646     private boolean hasValidGroupName() {
647         return mGroupNameView != null && !TextUtils.isEmpty(mGroupNameView.getText());
648     }
649 
hasNameChange()650     private boolean hasNameChange() {
651         return mGroupNameView != null &&
652                 !mGroupNameView.getText().toString().equals(mOriginalGroupName);
653     }
654 
hasMembershipChange()655     private boolean hasMembershipChange() {
656         return mListMembersToAdd.size() > 0 || mListMembersToRemove.size() > 0;
657     }
658 
659     /**
660      * Returns the group's new name or null if there is no change from the
661      * original name that was loaded for the group.
662      */
getUpdatedName()663     private String getUpdatedName() {
664         String groupNameFromTextView = mGroupNameView.getText().toString();
665         if (groupNameFromTextView.equals(mOriginalGroupName)) {
666             // No name change, so return null
667             return null;
668         }
669         return groupNameFromTextView;
670     }
671 
convertToArray(List<Member> listMembers)672     private static long[] convertToArray(List<Member> listMembers) {
673         int size = listMembers.size();
674         long[] membersArray = new long[size];
675         for (int i = 0; i < size; i++) {
676             membersArray[i] = listMembers.get(i).getRawContactId();
677         }
678         return membersArray;
679     }
680 
addExistingMembers(List<Member> members)681     private void addExistingMembers(List<Member> members) {
682 
683         // Re-create the list to display
684         mListToDisplay.clear();
685         mListToDisplay.addAll(members);
686         mListToDisplay.addAll(mListMembersToAdd);
687         mListToDisplay.removeAll(mListMembersToRemove);
688         mMemberListAdapter.notifyDataSetChanged();
689 
690 
691         // Update the autocomplete adapter (if there is one) so these contacts don't get suggested
692         if (mAutoCompleteAdapter != null) {
693             mAutoCompleteAdapter.updateExistingMembersList(members);
694         }
695     }
696 
addMember(Member member)697     private void addMember(Member member) {
698         // Update the display list
699         mListMembersToAdd.add(member);
700         mListToDisplay.add(member);
701         mMemberListAdapter.notifyDataSetChanged();
702 
703         // Update the autocomplete adapter so the contact doesn't get suggested again
704         mAutoCompleteAdapter.addNewMember(member.getContactId());
705     }
706 
removeMember(Member member)707     private void removeMember(Member member) {
708         // If the contact was just added during this session, remove it from the list of
709         // members to add
710         if (mListMembersToAdd.contains(member)) {
711             mListMembersToAdd.remove(member);
712         } else {
713             // Otherwise this contact was already part of the existing list of contacts,
714             // so we need to do a content provider deletion operation
715             mListMembersToRemove.add(member);
716         }
717         // In either case, update the UI so the contact is no longer in the list of
718         // members
719         mListToDisplay.remove(member);
720         mMemberListAdapter.notifyDataSetChanged();
721 
722         // Update the autocomplete adapter so the contact can get suggested again
723         mAutoCompleteAdapter.removeMember(member.getContactId());
724     }
725 
726     /**
727      * The listener for the group metadata (i.e. group name, account type, and account name) loader.
728      */
729     private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener =
730             new LoaderCallbacks<Cursor>() {
731 
732         @Override
733         public CursorLoader onCreateLoader(int id, Bundle args) {
734             return new GroupMetaDataLoader(mContext, mGroupUri);
735         }
736 
737         @Override
738         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
739             bindGroupMetaData(data);
740 
741             // Load existing members
742             getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null,
743                     mGroupMemberListLoaderListener);
744         }
745 
746         @Override
747         public void onLoaderReset(Loader<Cursor> loader) {}
748     };
749 
750     /**
751      * The loader listener for the list of existing group members.
752      */
753     private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener =
754             new LoaderCallbacks<Cursor>() {
755 
756         @Override
757         public CursorLoader onCreateLoader(int id, Bundle args) {
758             return GroupMemberLoader.constructLoaderForGroupEditorQuery(mContext, mGroupId);
759         }
760 
761         @Override
762         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
763             List<Member> listExistingMembers = new ArrayList<Member>();
764             data.moveToPosition(-1);
765             while (data.moveToNext()) {
766                 long contactId = data.getLong(GroupEditorQuery.CONTACT_ID);
767                 long rawContactId = data.getLong(GroupEditorQuery.RAW_CONTACT_ID);
768                 String lookupKey = data.getString(GroupEditorQuery.CONTACT_LOOKUP_KEY);
769                 String displayName = data.getString(GroupEditorQuery.CONTACT_DISPLAY_NAME_PRIMARY);
770                 String photoUri = data.getString(GroupEditorQuery.CONTACT_PHOTO_URI);
771                 listExistingMembers.add(new Member(rawContactId, lookupKey, contactId,
772                         displayName, photoUri));
773             }
774 
775             // Update the display list
776             addExistingMembers(listExistingMembers);
777 
778             // No more updates
779             // TODO: move to a runnable
780             getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS);
781         }
782 
783         @Override
784         public void onLoaderReset(Loader<Cursor> loader) {}
785     };
786 
787     /**
788      * The listener to load a summary of details for a contact.
789      */
790     // TODO: Remove this step because showing the aggregate contact can be confusing when the user
791     // just selected a raw contact
792     private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener =
793             new LoaderCallbacks<Cursor>() {
794 
795         private long mRawContactId;
796 
797         @Override
798         public CursorLoader onCreateLoader(int id, Bundle args) {
799             String memberId = args.getString(MEMBER_LOOKUP_URI_KEY);
800             mRawContactId = args.getLong(MEMBER_RAW_CONTACT_ID_KEY);
801             return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId),
802                     PROJECTION_CONTACT, null, null, null);
803         }
804 
805         @Override
806         public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
807             if (!cursor.moveToFirst()) {
808                 return;
809             }
810             // Retrieve the contact data fields that will be sufficient to update the adapter with
811             // a new entry for this contact
812             long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
813             String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
814             String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
815             String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX);
816             getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER);
817             Member member = new Member(mRawContactId, lookupKey, contactId, displayName, photoUri);
818             addMember(member);
819         }
820 
821         @Override
822         public void onLoaderReset(Loader<Cursor> loader) {}
823     };
824 
825     /**
826      * This represents a single member of the current group.
827      */
828     public static class Member implements Parcelable {
829 
830         // TODO: Switch to just dealing with raw contact IDs everywhere if possible
831         private final long mRawContactId;
832         private final long mContactId;
833         private final Uri mLookupUri;
834         private final String mDisplayName;
835         private final Uri mPhotoUri;
836         private final String mLookupKey;
837 
Member(long rawContactId, String lookupKey, long contactId, String displayName, String photoUri)838         public Member(long rawContactId, String lookupKey, long contactId, String displayName,
839                 String photoUri) {
840             mRawContactId = rawContactId;
841             mContactId = contactId;
842             mLookupKey = lookupKey;
843             mLookupUri = Contacts.getLookupUri(contactId, lookupKey);
844             mDisplayName = displayName;
845             mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null;
846         }
847 
getRawContactId()848         public long getRawContactId() {
849             return mRawContactId;
850         }
851 
getContactId()852         public long getContactId() {
853             return mContactId;
854         }
855 
getLookupUri()856         public Uri getLookupUri() {
857             return mLookupUri;
858         }
859 
getLookupKey()860         public String getLookupKey() {
861             return mLookupKey;
862         }
863 
getDisplayName()864         public String getDisplayName() {
865             return mDisplayName;
866         }
867 
getPhotoUri()868         public Uri getPhotoUri() {
869             return mPhotoUri;
870         }
871 
872         @Override
equals(Object object)873         public boolean equals(Object object) {
874             if (object instanceof Member) {
875                 Member otherMember = (Member) object;
876                 return Objects.equal(mLookupUri, otherMember.getLookupUri());
877             }
878             return false;
879         }
880 
881         @Override
hashCode()882         public int hashCode() {
883             return mLookupUri == null ? 0 : mLookupUri.hashCode();
884         }
885 
886         // Parcelable
887         @Override
describeContents()888         public int describeContents() {
889             return 0;
890         }
891 
892         @Override
writeToParcel(Parcel dest, int flags)893         public void writeToParcel(Parcel dest, int flags) {
894             dest.writeLong(mRawContactId);
895             dest.writeLong(mContactId);
896             dest.writeParcelable(mLookupUri, flags);
897             dest.writeString(mLookupKey);
898             dest.writeString(mDisplayName);
899             dest.writeParcelable(mPhotoUri, flags);
900         }
901 
Member(Parcel in)902         private Member(Parcel in) {
903             mRawContactId = in.readLong();
904             mContactId = in.readLong();
905             mLookupUri = in.readParcelable(getClass().getClassLoader());
906             mLookupKey = in.readString();
907             mDisplayName = in.readString();
908             mPhotoUri = in.readParcelable(getClass().getClassLoader());
909         }
910 
911         public static final Parcelable.Creator<Member> CREATOR = new Parcelable.Creator<Member>() {
912             @Override
913             public Member createFromParcel(Parcel in) {
914                 return new Member(in);
915             }
916 
917             @Override
918             public Member[] newArray(int size) {
919                 return new Member[size];
920             }
921         };
922     }
923 
924     /**
925      * This adapter displays a list of members for the current group being edited.
926      */
927     private final class MemberListAdapter extends BaseAdapter {
928 
929         private boolean mIsGroupMembershipEditable = true;
930 
931         @Override
getView(int position, View convertView, ViewGroup parent)932         public View getView(int position, View convertView, ViewGroup parent) {
933             View result;
934             if (convertView == null) {
935                 result = mLayoutInflater.inflate(mIsGroupMembershipEditable ?
936                         R.layout.group_member_item : R.layout.external_group_member_item,
937                         parent, false);
938             } else {
939                 result = convertView;
940             }
941             final Member member = getItem(position);
942 
943             QuickContactBadge badge = (QuickContactBadge) result.findViewById(R.id.badge);
944             badge.assignContactUri(member.getLookupUri());
945 
946             TextView name = (TextView) result.findViewById(R.id.name);
947             name.setText(member.getDisplayName());
948 
949             View deleteButton = result.findViewById(R.id.delete_button_container);
950             if (deleteButton != null) {
951                 deleteButton.setOnClickListener(new OnClickListener() {
952                     @Override
953                     public void onClick(View v) {
954                         removeMember(member);
955                     }
956                 });
957             }
958             DefaultImageRequest request = new DefaultImageRequest(member.getDisplayName(),
959                     member.getLookupKey(), true /* isCircular */);
960             mPhotoManager.loadPhoto(badge, member.getPhotoUri(),
961                     ViewUtil.getConstantPreLayoutWidth(badge), false, true /* isCircular */,
962                             request);
963             return result;
964         }
965 
966         @Override
getCount()967         public int getCount() {
968             return mListToDisplay.size();
969         }
970 
971         @Override
getItem(int position)972         public Member getItem(int position) {
973             return mListToDisplay.get(position);
974         }
975 
976         @Override
getItemId(int position)977         public long getItemId(int position) {
978             return position;
979         }
980 
setIsGroupMembershipEditable(boolean editable)981         public void setIsGroupMembershipEditable(boolean editable) {
982             mIsGroupMembershipEditable = editable;
983         }
984     }
985 }
986