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.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 
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 
517     @Override
onOptionsItemSelected(MenuItem item)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 
revert()526     private boolean revert() {
527         if (!hasNameChange() && !hasMembershipChange()) {
528             doRevertAction();
529         } else {
530             CancelEditDialogFragment.show(this);
531         }
532         return true;
533     }
534 
doRevertAction()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 
show(GroupEditorFragment fragment)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
onCreateDialog(Bundle savedInstanceState)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      */
save()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 
onSaveCompleted(boolean hadChanges, Uri groupUri)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 
hasValidGroupName()664     private boolean hasValidGroupName() {
665         return mGroupNameView != null && !TextUtils.isEmpty(mGroupNameView.getText());
666     }
667 
hasNameChange()668     private boolean hasNameChange() {
669         return mGroupNameView != null &&
670                 !mGroupNameView.getText().toString().equals(mOriginalGroupName);
671     }
672 
hasMembershipChange()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      */
getUpdatedName()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 
convertToArray(List<Member> listMembers)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 
addExistingMembers(List<Member> members)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 
addMember(Member member)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 
removeMember(Member member)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 
Member(long rawContactId, String lookupKey, long contactId, String displayName, String photoUri)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 
getRawContactId()866         public long getRawContactId() {
867             return mRawContactId;
868         }
869 
getContactId()870         public long getContactId() {
871             return mContactId;
872         }
873 
getLookupUri()874         public Uri getLookupUri() {
875             return mLookupUri;
876         }
877 
getLookupKey()878         public String getLookupKey() {
879             return mLookupKey;
880         }
881 
getDisplayName()882         public String getDisplayName() {
883             return mDisplayName;
884         }
885 
getPhotoUri()886         public Uri getPhotoUri() {
887             return mPhotoUri;
888         }
889 
890         @Override
equals(Object object)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
hashCode()900         public int hashCode() {
901             return mLookupUri == null ? 0 : mLookupUri.hashCode();
902         }
903 
904         // Parcelable
905         @Override
describeContents()906         public int describeContents() {
907             return 0;
908         }
909 
910         @Override
writeToParcel(Parcel dest, int flags)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 
Member(Parcel in)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
getView(int position, View convertView, ViewGroup parent)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
getCount()985         public int getCount() {
986             return mListToDisplay.size();
987         }
988 
989         @Override
getItem(int position)990         public Member getItem(int position) {
991             return mListToDisplay.get(position);
992         }
993 
994         @Override
getItemId(int position)995         public long getItemId(int position) {
996             return position;
997         }
998 
setIsGroupMembershipEditable(boolean editable)999         public void setIsGroupMembershipEditable(boolean editable) {
1000             mIsGroupMembershipEditable = editable;
1001         }
1002     }
1003 }
1004